跳到主要内容

go-channel

go有个经典的话,不要通过共享内存来通信,而应该通过通信来共享数据。channel就是这个思想重要体现

  • channel 是 Go 的内置类型, 可以理解为通道
    • 利用channel通道在协程之间通信
  • 用于goroutine(协程并发任务)之间传数据
  • channel也用于同步协调,在多个任务之间通信时候,同步和协调多个并发任务的执行
    • 当任务接收channel数据时,如果还没有数据,当前goroutine 会阻塞等待
  • 同步协调理解为:让多个并发任务在合适的时机等待,按照依赖关系配合起来
  • channel理解为:任务之间通信 + 同步协调

channel定义​

  • chan int 是一个整体的类型,表示变量类型
    • 表示可以传递 int 的 channel 类型
go
// 传递int类型数据的channel
var ch1 chan int
// 传递string类型数据channel
var ch2 chan string

// 使用make创建
ch := make(chan int)

channel 发送和接收数据​

  • <- 用于 channel 发送和接收数据的操作符。
go
// 把10发送到ch这个channel里
ch <- 10

// 从 ch 这个 channel 接收数据,并赋值给 num
num := <-ch

有缓冲 channel 和无缓冲 channel​

无缓冲 channel 在发送和接收双方都准备好时才能完成通信,因此同步性强;有缓冲 channel 内部可以暂存一定数量的数据

  • make(chan T) 不写容量,就是无缓冲
  • make(chan T, n) 的 n 是缓冲区容量

有缓冲​

  • 数据暂时放在缓冲区里,需要定义缓冲容量
  • 数据即使还没人接收,也不会阻塞,不会造成死锁
  • 当缓冲容量数据放满了,也会造成阻塞,因为缓冲满了,继续发送数据,也放不进去,发送方就会阻塞,直到有接收方收到数据,来消费这个数据才会腾出这个位置,后续阻塞的才会继续执行
    • 接收缓冲区的数据,如果缓冲区空的,接收时,也会阻塞,等待有数据,才会继续执行
  • 适合任务队列、并发限流场景

无缓冲​

  • 无缓冲 channel:具备同步作用
  • 无缓冲不暂存数据,接收和发送数据需要同时同步进行
  • 可以理解为无缓冲 channel的同步类似 打电话,需要双方都准备好

channel死锁​

Go 的 channel 死锁通常发生在 goroutine 永久等待 channel 的发送或接收,而系统中没有其他 goroutine 能解除这个阻塞时

  • 所有 goroutine 都在等,但没有任何一个 goroutine 能继续往下执行
  • channel 的发送和接收没有匹配上问题
  • 比如常见的场景
    • 定义了无缓冲 channel,发送了数据,但是没有接收,就会阻塞
    • 包括无缓冲 channel 没有对应接收方、有缓冲 channel 写满后继续写、空 channel 继续读取、range channel 未关闭

channel无缓冲死锁问题​

  • 无缓冲 channel:发送时,必须同时有接收方准备接收,否则发送操作就会阻塞,导致死锁
  • 有缓冲 channel:只要缓冲区还没满,发送就不会阻塞;缓冲区满了之后,继续发送才会阻塞

无缓冲 channel:没人接收就阻塞 有缓冲 channel:缓冲区满了才阻塞 有缓冲 channel 接收时:缓冲区空了就阻塞

range channel 未关闭理解​

  • for v := range ch 不知道什么时候该结束,它会一直等待 channel 后续是否还有数据
  • 发送方不再发数据时,应通过close(ch) 来关闭通道
  • range 不是读到 channel 为空就结束,而是读到 channel 已关闭且数据已经全部消费完才结束
go
package main

import "fmt"

func main() {
ch := make(chan int, 3)

ch <- 1
ch <- 2
ch <- 3
close(ch) // 关闭后不能再发送;还能继续接收剩余数据

for v := range ch {
fmt.Println(v)
}
}

注意问题​

  • goroutine任务之间通信问题
    • 接收方如果先执行,而发送方还没准备好数据,接收方会阻塞等待

例子理解​

并发任务例子​

  • 两个 goroutine 并发执行任务,然后通过 channel 把结果传回 main
  • 注意这个例子中,任务1和任务2输出的顺序是不确定的,因为这2个任务都是并发执行,哪个任务谁先完成,就先把结果发送给channel
go
package main

import "fmt"

func main() {
ch := make(chan string)

// 并发任务 1
go func() {
ch <- "任务1完成"
}()

// 并发任务 2
go func() {
ch <- "任务2完成"
}()

// 接收两个并发任务返回的数据
msg1 := <-ch
msg2 := <-ch

fmt.Println(msg1)
fmt.Println(msg2)
}

任务之间传递数据​

  • 问题:假如任务2先执行,任务1还没执行完,那么任务2如何拿到任务1的数据?
    • channel 重要的地方:接收方如果先执行,而发送方还没准备好数据,接收方会阻塞等待
    • 如果任务2先执行,那么任务2会先等待任务1,直到任务1执行完成,接收到数据
    • 因为channel 不只是“传数据”,还会帮 goroutine 做同步协调
go
package main

import "fmt"

func main() {
ch := make(chan string)

// 任务1:发送数据
go func() {
ch <- "任务1产生的数据"
}()

// 任务2:接收任务1的数据
go func() {
msg := <-ch
fmt.Println("任务2收到:", msg)
}()

fmt.Scanln()
}