Go的channel用法和那些坑
channel 是 goroutine 之间通信和同步的管道,make 创建、箭头收发,并发安全不用加锁。分无缓冲(强同步,收发必须碰面)和有缓冲(可暂存)两种。关闭记牢:发送方关、关后不能再发、读到零值用 v,ok 区分。多路监听用 select 加 default 做非阻塞。坑在死锁、泄漏、向关闭channel发送、nil channel 永久阻塞。
Go 并发有句口号很有名:不要用共享内存来通信,而要用通信来共享内存。
这句话的主角,就是 channel(管道)。
它是 goroutine 之间传数据、做同步的核心工具。但新手用 channel 很容易踩坑:死锁、往关闭的 channel 写、goroutine 泄漏……这篇把 channel 怎么用、几个必知的坑,一次讲透。
channel 是什么
channel 可以理解成 goroutine 之间的一根"管道":一头塞数据进去,另一头取出来。
它天生就是并发安全的,多个 goroutine 同时读写同一个 channel,不用自己加锁。
创建用 make,操作用 <- 这个箭头符号:
// 创建一个传 int 的 channel
ch := make(chan int)
// 发送:箭头指向 channel
ch <- 100
// 接收:箭头从 channel 指出来
v := <-ch
箭头方向好记:数据往哪流,箭头就指向哪。ch <- 100 是把 100 塞进 ch,v := <-ch 是从 ch 取出来给 v。
无缓冲 vs 有缓冲
channel 分两种,行为差别很大,这是理解它的关键。
无缓冲 channel(不传容量),发送和接收必须"同时在场",否则阻塞:
ch := make(chan int) // 无缓冲
// ch <- 1 // 如果没有另一个 goroutine 在接收,这行会永远卡住
无缓冲 channel 是"一手交钱一手交货":发送方塞数据时,必须有接收方正在等着收,双方才能继续;否则谁先到谁等着。所以它天然带同步语义。
有缓冲 channel(传个容量),有个"缓冲区",没满就能一直塞,不用等接收方:
ch := make(chan int, 3) // 容量 3 的缓冲
ch <- 1 // 不阻塞
ch <- 2 // 不阻塞
ch <- 3 // 不阻塞,缓冲满了
// ch <- 4 // 这时才阻塞,等有人取走才能再塞
有缓冲像个"快递柜":柜子没满,发件人塞进去就走,不用等收件人。满了才得等。
一句话区分:无缓冲重同步(强制双方碰面),有缓冲重解耦(允许暂存)。
一个完整例子
把它俩串起来,看个生产者-消费者的例子:
func main() {
ch := make(chan int, 5) // 带缓冲,生产者不用每个都等消费者
// 生产者:塞 5 个数进去
go func() {
for i := 1; i <= 5; i++ {
ch <- i
fmt.Println("生产:", i)
}
close(ch) // 生产完,关闭 channel,告诉消费者没了
}()
// 消费者:用 range 不断取,直到 channel 关闭
for v := range ch {
fmt.Println("消费:", v)
}
fmt.Println("全部消费完")
}
这里有两个要点:
close(ch):生产者活干完了,关闭 channel,相当于说"后面没货了"。for v := range ch:消费者用 range 遍历,channel 一关、里面的数据也取完了,循环自动结束。不用自己判断什么时候停。
关闭 channel 的规矩
close 看着简单,规矩不少,踩错就 panic。
规矩一:只有发送方能关,接收方别关。
channel 是单向告知"没货了"的,这事该由生产者说。消费者去关会乱套。
规矩二:往已关闭的 channel 发送,直接 panic。
ch := make(chan int, 1)
close(ch)
ch <- 1 // panic: send on closed channel
规矩三:从已关闭的 channel 还能读,读完剩余数据后,读到的是零值。
用双返回值能区分"是真数据"还是"channel 关了给的零值":
v, ok := <-ch
// ok == true:正常数据
// ok == false:channel 已关闭且数据取完了,v 是零值
if !ok {
fmt.Println("channel 关了")
}
规矩四:别重复 close,关两次也 panic。
用 select 处理多个 channel
实际场景常常要同时盯着好几个 channel,这时候用 select,它会等多个 channel,哪个先就绪走哪个(上一篇讲超时也用的它)。
select {
case v := <-ch1:
fmt.Println("ch1 来了:", v)
case v := <-ch2:
fmt.Println("ch2 来了:", v)
case ch3 <- 100:
fmt.Println("往 ch3 发送成功")
default:
// 所有 case 都没就绪时走这里,避免阻塞
fmt.Println("都没动静,先干别的")
}
加个 default 分支,就能让 select 变成"非阻塞"的:有就处理,没有也不干等。这招在轮询、心跳检测里很常用。
几个经典坑
把血泪教训归纳一下:
- 死锁:无缓冲 channel 发送却没人接收(或反过来),主 goroutine 卡死,Go 直接报
fatal error: all goroutines are asleep - deadlock。最常见于忘了开接收的 goroutine。 - goroutine 泄漏:goroutine 卡在往没人收的 channel 发送上,永远退不出,内存一直占着。记得给 channel 合理的缓冲,或用 context 兜底退出。
- 向关闭的 channel 发送:panic。设计上让关闭和发送分清职责。
- nil channel:对
nilchannel 读写会永久阻塞(不是 panic)。偶尔被利用来在 select 里"禁用"某个分支,但没意识到时就是莫名其妙卡住。
小结
channel 是 goroutine 之间通信和同步的管道,make 创建、<- 收发,并发安全不用自己加锁。
分无缓冲(强同步,收发必须碰面)和有缓冲(可暂存,满了才阻塞)两种,按"要不要强制双方同步"来选。
关闭记牢:发送方关、关后不能再发、读得到零值要用 v, ok 区分、别重复关。多路监听用 select,加 default 可做非阻塞。
坑主要集中在死锁、goroutine 泄漏、向关闭的 channel 发送、nil channel 永久阻塞这几处,想清楚"谁发谁收、什么时候关"基本都能避开。
把 channel 玩明白,Go 那句"用通信共享内存"才算真正落地。
