5.2 Channel:通信、同步与高级模式


5.2 Channel:通信、同步与高级模式

本节摘要:channel 是类型化的并发安全队列,也是 goroutine 间传递数据与信号的桥梁。本节覆盖声明与初始化、发送接收的阻塞语义、无缓冲与有缓冲的同步/异步分野、关闭的正确姿势与 range 遍历、单向通道的 API 约束,以及信号通知、扇入、退出信号、并发限流等高级模式。

核心问题

阅读完本节,你应当能够:

  1. 预测无缓冲与有缓冲 channel 上每个收发操作是否阻塞;
  2. 用关闭加 range 的组合安全地消费数据流;
  3. 用单向通道约束函数的通信方向;
  4. 实现信号通知、结果收集、扇入合并与并发限流;
  5. 避开向已关闭通道发送、重复关闭等致命操作。

一、本质与语法

channel 是一个带类型的先进先出队列,收发两端在任何数量的 goroutine 间都是并发安全的(channel 自身保证,不需要加锁)。三种声明:

var c1 chan int // nil通道,未初始化 c2 := make(chan int) // 无缓冲 c3 := make(chan int, 10) // 缓冲容量10

操作就三个:箭头向里发送 c2 <- 1,箭头向外接收 <-c2,内建 close 关闭。nil 通道的语义是"永久阻塞"——收发都卡死,这个特性后面在 select 里有妙用。

二、阻塞语义:无缓冲与有缓冲

无缓冲通道是同步点。发送方阻塞直到接收方就位,接收方阻塞直到发送方就位——两边"握手"成功才同时继续。它不存数据,它是约会:

done := make(chan bool) go func() { work() done <- true // 等 main来收,才返回 }() <-done // main阻塞等工作完成

有缓冲通道是异步队列。缓冲没满,发送立即返回;满了才阻塞。接收在缓冲非空时立即成功,空了阻塞。容量是"允许超前多少步"的额度:

tasks := make(chan int, 3) tasks <- 1 // 立即返回(占用1格) tasks <- 2 tasks <- 3 // tasks <- 4 // 此处会阻塞,直到有人取走一格
操作 nil 通道 无缓冲 有缓冲(未满/非空) 有缓冲(满/空) 已关闭
发送 永久阻塞 等接收方 立即 阻塞 panic
接收 永久阻塞 等发送方 立即 阻塞 立即返回缓冲剩余,之后返回零值

这张表值得背下来——Go 并发面试的一半考它。

图:无缓冲握手与有缓冲队列

图:无缓冲握手与有缓冲队列

三、关闭与遍历

close 表示"没有更多数据了"。接收方两种感知方式:双返回值 v, ok := <-c(ok 为假即已关闭且排空),或者更惯用的 range——通道关闭后循环自动结束:

jobs := make(chan int, 5) go func() { for i := 1; i <= 5; i++ { jobs <- i } close(jobs) // 发送方负责关闭 }() for j := range jobs { // 关闭即退出循环 fmt.Println("处理", j) }

关闭不是"必须的善后":垃圾回收会回收不再使用的通道,close 的唯一目的是通知接收方数据流结束。不需要通知就不关;拿不准就不关——泄漏一个没人等的 channel 比误关导致 panic 温柔得多。

四、单向通道:把方向写进签名

func produce(out chan<- int) { ... } // 只能发 func consume(in <-chan int) { ... } // 只能收

双向通道可以赋给单向,反过来不行。价值在 API 自解释:参数类型直接告诉调用方"我只发不收",编译器替你站岗。

五、高级模式四则

模式一:完成信号(前文 done 通道),无缓冲、发一个布尔或直接 close(done)——关闭广播给所有接收方,一发多收用关闭最省。

模式二:结果收集。开 N 个 worker 各算各的,结果统一送进一个通道,主流程收满 N 个。通道自带锁,不需要任何额外同步。

模式三:扇入合并。多个来源通道的数据汇入一条流,用 select 轮流搬(见下节)或每个来源一个搬运 goroutine。

模式四:并发限流。缓冲通道当信号量:

sem := make(chan struct{}, 3) // 最多3并发 for _, t := range tasks { sem <- struct{}{} // 满3则阻塞,天然限流 go func(id int) { defer func() { <-sem }() // 干完释放一格 work(id) }(t) }

空结构体零字节,是"只占坑不装货"的标准选择。

⚠️ 常见坑:主流程用 time 加睡眠"等" goroutine 干完——竞态赌博不是同步。要么 WaitGroup,要么收结果通道,让完成可观测。

💡 关键直觉:channel 是传送带不是储物柜——设计时先想"谁给谁、什么时候给、什么时候停",容量只是节奏调节旋钮。

六、模式组合全景

问答补遗。channel 容量选多大? 默认零(显式同步最易推理);确有速率差再给缓冲,容量以"下游消费一批的量"为参考,别拍脑袋给一万。怎么知道 channel 该关了? 只有"多接收方等结束通知"时才需要关;一对一传值甚至可以从不关闭,交给 GC。发送方多个谁关? 谁都不许擅关——多发送方场景由"最后一个发完的人"或统一的协调者关闭,或干脆改用 WaitGroup 加集中发送。channel 会内存泄漏吗? 没人收也没人发的满缓冲通道会连数据一起被 GC 收走;真正泄漏的是"卡在收发上的 goroutine"——对象是通道,病因在退出设计。

重点提炼

  • 无缓冲是握手,发送接收双向就位才放行;有缓冲是队列,满发阻塞、空收阻塞。
  • nil 通道永久阻塞,在 select 里可当"禁用分支"用。
  • 关闭三规则:发送方关、已关闭再发送 panic、关闭后排空再返零值加假。
  • range 消费通道是标准姿势,关闭即结束。
  • 单向通道把通信方向写进函数签名。
  • 四大模式:完成信号、结果收集、扇入、缓冲信号量限流。
  • 睡眠不是同步,完成必须可观测。

下一节给通道装上调度台:select。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U