深色模式
Go语言之 channel
概述
channel 是 Go 并发里最显眼的工具,但它不只是“能传值的队列”。channel 真正同时做了两件事:传递数据,以及在 goroutine 之间建立时序上的同步关系。
这一篇从 channel 自身展开,重点不再是并发地图,而是 channel 的具体行为:无缓冲和有缓冲到底差在哪,何时该关闭,select 在协调什么,哪些写法最容易把 goroutine 卡死。
channel 的基本形式
创建一个 channel:
go
ch := make(chan int)发送值:
go
ch <- 42接收值:
go
v := <-ch无缓冲 channel 更像同步交接
go
ch := make(chan int)无缓冲 channel 没有中间缓冲区,所以:
- 发送方要等接收方准备好
- 接收方也要等发送方准备好
例如:
go
go func() {
ch <- 42
}()
v := <-ch
fmt.Println(v)这里发送和接收像是一次当场交接。
有缓冲 channel 更像队列
go
ch := make(chan int, 2)这时 channel 有容量为 2 的缓冲区:
- 缓冲未满时,发送方可以先放值进去
- 缓冲不空时,接收方可以直接取值
例如:
go
ch := make(chan int, 2)
ch <- 1
ch <- 2前两次发送不会阻塞,但再发第三个值时就会等。
channel 的方向可以限制能力
函数参数里经常会把 channel 写成单向:
go
func send(ch chan<- int) {
ch <- 1
}
func recv(ch <-chan int) int {
return <-ch
}这样做的价值在于函数边界更清楚。
关闭 channel 是一种发送方声明
关闭 channel:
go
close(ch)它的意义不是“立即销毁 channel”,而是“发送方声明,后面不会再有新值发送了”。
这会带来两条很重要的规则:
- 向已关闭 channel 发送值会 panic
- 从已关闭 channel 接收值,如果缓冲已空,会得到零值和
ok == false
例如:
go
v, ok := <-ch
if !ok {
fmt.Println("closed")
}谁来关闭 channel,要先想清楚
最稳的经验通常是:
- 由发送方关闭
- 由最后一个发送者关闭
接收方一般不该随手关闭 channel,因为它并不天然知道还有没有别的发送者在路上。
range 适合消费一串直到结束的数据
go
for v := range ch {
fmt.Println(v)
}这个写法会持续接收,直到 channel 被关闭且缓冲耗尽。
select 协调的是多个通道事件
例如:
go
select {
case v := <-dataCh:
fmt.Println(v)
case <-doneCh:
return
}select 的意义是:在多个可能的通道操作里,选择当前可执行的那一个。
如果加上 default:
go
select {
case v := <-ch:
fmt.Println(v)
default:
fmt.Println("no data")
}那它就会在当前没有可执行分支时立即走 default。
channel 最常见的几种协作模式
一发一收
用无缓冲 channel 做显式同步点。
任务队列
用有缓冲 channel 把任务送给多个 worker。
广播结束
通过关闭某个 done channel,通知多个 goroutine 停止工作。
流式消费
发送方不断写入,接收方用 range 持续读取直到关闭。
最容易踩的几个坑
没有接收者,却在发
无缓冲 channel 上如果没有接收者准备好,发送方会卡住。
没有发送者,却在收
接收方会一直等,直到有人发值或 channel 被关闭。
多个发送方都想关
这类情况很容易 panic。关闭责任必须先定义清楚。
以为关闭后还能继续发
关闭的含义就是“不再发送”。继续发送一定出错。
一条够用的判断线
用 channel 时,先问自己:
- 我需要的是同步交接,还是缓冲队列。
- 谁负责发送,谁负责接收。
- 谁负责关闭,以及它凭什么知道该关了。
- 当前问题该用单个 channel,还是该配合
select协调多个事件。
