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:对 nil channel 读写会永久阻塞(不是 panic)。偶尔被利用来在 select 里"禁用"某个分支,但没意识到时就是莫名其妙卡住。

小结

channel 是 goroutine 之间通信和同步的管道,make 创建、<- 收发,并发安全不用自己加锁。

分无缓冲(强同步,收发必须碰面)和有缓冲(可暂存,满了才阻塞)两种,按"要不要强制双方同步"来选。

关闭记牢:发送方关、关后不能再发、读得到零值要用 v, ok 区分、别重复关。多路监听用 select,加 default 可做非阻塞。

坑主要集中在死锁、goroutine 泄漏、向关闭的 channel 发送、nil channel 永久阻塞这几处,想清楚"谁发谁收、什么时候关"基本都能避开。

把 channel 玩明白,Go 那句"用通信共享内存"才算真正落地。

更多推荐

章节目录