Go channel
channel 在 goroutine 之间传递值并建立同步点:缓冲与无缓冲、关闭与排空、nil 阻塞、所有权与 happens-before。
[!info] 关联笔记
Go channel
这个概念为什么会出现
多个 goroutine 若只靠共享内存 + 锁,正确性与可读性成本都高。Go 把进程内的通信做成类型系统的一部分:
ch := make(chan int)
ch <- 1
v := <-ch
channel 同时做两件事:传值与同步。它不是 Kafka 式分布式队列,而是语言级并发原语。
[!abstract] 一句话理解 channel 是类型化的通信管道;发送与接收在规则满足时交接数据,并在 Go 内存模型下建立 happens-before 关系。关闭表示“不会再有新值”,通常由发送方负责。
最小可运行示例
先把示例放进业务场景,再看代码:
场景:健康检查探针把结果交回主流程
服务启动时要跑一次依赖探测(ping Redis/DB)。
探测在后台 goroutine 做,主流程必须拿到结果字符串后才能决定是否对外服务。
用无缓冲 channel:
- 传值:把
"pong"/ 错误信息递回来 - 同步:发送与接收握手——主流程打印/判断一定发生在探测方写入之后
这比“共享变量 + 睡一会再读”可靠得多。
package main
import "fmt"
// probeDependency 模拟“后台探测依赖是否存活”。
// 业务意图:慢 I/O 放在 goroutine;结果只通过 out 交回,不写共享变量。
// 教学点:ch <- v 在无缓冲 channel 上会等到有人接收。
func probeDependency(out chan<- string) {
// 真实项目里:ping Redis、HEAD 健康 URL 等。
// 成功就送 "pong";失败可送 "down: ..." 或改用 struct{ok,err}。
out <- "pong"
}
func main() {
// 无缓冲 channel:发送与接收必须配对,本身就是同步点。
result := make(chan string)
// 启动探针;main 继续往下走到接收处等待。
go probeDependency(result)
// 阻塞直到探针写入;读到的值一定 happens-after 发送侧赋值完成。
status := <-result
fmt.Println("dependency:", status) // dependency: pong
}
建议运行:
go run .
期望输出:
dependency: pong
结合场景再看三个关注点
-
channel = 传值 + 同步
主流程不是“猜探针写完没有”,而是在<-result上明确等待。 -
无缓冲要求双方握手
适合“结果必须被看见才继续”的启动探针、一次性信号。 -
所有权清晰:谁发送、谁接收
本例只有探针发送、main 接收;关闭与多发送方规则见后文。
核心概念与准确模型
创建
make(chan T) // 无缓冲
make(chan T, n) // 缓冲容量 n
var ch chan T // nil channel
单向类型用于 API 边界:
func produce(out chan<- int) {}
func consume(in <-chan int) {}
无缓冲 vs 有缓冲
| 无缓冲 | 有缓冲 | |
|---|---|---|
| 发送 | 需接收方准备好 | 缓冲未满即可返回 |
| 接收 | 需发送方准备好 | 缓冲非空即可返回 |
| 典型用途 | 同步交接、信号 | 平滑速率、有限队列 |
缓冲容量是本地资源上限,不是无限队列。
关闭与排空
close(ch)
v, ok := <-ch // ok=false 表示已关闭且无更多值
for v := range ch {
// 直到关闭并排空
}
规则:
- 只有发送方(所有权方)应关闭
- 向已关闭 channel 发送 → panic
- 重复关闭 → panic
- 接收已关闭:先读完剩余缓冲,再得到零值且
ok=false - 关闭不是“释放所有资源”的通用析构,而是“流结束”信号
状态表
| 状态 | 接收 | 发送 | close |
|---|---|---|---|
| nil | 永久阻塞 | 永久阻塞 | panic |
| 打开,可进行 | 得值 | 成功 | 成功 |
| 已关闭,仍有缓冲 | 得缓冲值 | panic | panic |
| 已关闭,已排空 | 零值,ok=false | panic | panic |
与内存模型
channel 通信建立同步:发送 happens-before 对应接收完成。详见 Go Memory Model — Channel communication 与 go-happens-before-and-synchronization。
nil channel 与 select
把 ch = nil 可禁用 select 的某个 case(永久不选中)。这是取消某路通信的惯用技巧。
设计动机
口号式表述常写作 Do not communicate by sharing memory; share memory by communicating。更精确地说:
- 先用数据流表达并发结构
- 锁仍适用于共享状态结构
- channel 让“谁在何时可见什么”更局部
边界情况
- 死锁:无缓冲互相等待、main 只发送不接收等,可能被运行时检测并 fail。
- 泄漏:发送方无人接、接收方无人发。
- 关闭权争夺:多发送方时需额外协调(如 WaitGroup 后再 close)。
- 传递指针:channel 传的是值;指针指向的数据仍共享,需约定所有权。
- 不是广播:默认一对一交接;广播需多 channel 或其他结构。
常见误区
[!warning] 常见误区:接收方 close 错误:消费者关闭共享 channel。
正确:发送所有权方关闭;或文档明确单一 closer。
[!warning] 常见误区:用 channel 替代所有锁 错误:复杂共享图硬套 channel 更乱。
正确:结构所有权清晰时用 channel;共享 map/计数器用 mutex/原子。
[!warning] 常见误区:无限缓冲幻想 错误:
make(chan T, 1<<30)当消息队列。
正确:有界 + 背压 + 超时/取消。
工程实践
- 所有权写进函数签名(单向 channel)
- 关闭即结束流,与
range搭配 - select + 超时/ctx 防永久阻塞(go-select)
- 错误与值一起传:
struct { Val T; Err error }或双 channel 并文档化 - 测试并发用
-race与确定性同步,避免time.Sleep当正确性条件
可验证实验
- 无缓冲发送无接收 → 死锁。
close后range排空。- 向关闭 channel 发送 → panic。
- nil channel 在 select 中被禁用。
- 缓冲为 1 时连续发送第二次是否阻塞。
本节总结
- 本质:类型化进程内管道 + 同步点。
- 关键规则:关闭所有权、状态表、nil 阻塞、内存模型同步。
- 最易错:错关、泄漏、当分布式队列。
- 下一步:select 与取消组合。
自测题
概念题
- 无缓冲 channel 的同步含义是什么?
- 谁应该 close?
- 已关闭 channel 上接收的
ok何时为 false?
代码推理题
ch := make(chan int, 1)
ch <- 1
close(ch)
v1, ok1 := <-ch
v2, ok2 := <-ch
fmt.Println(v1, ok1, v2, ok2)
工程思考题
多个 worker 写入同一结果 channel,如何安全关闭?
参考答案
展开
-
发送与接收完成形成交接与同步。
-
发送所有权方。
-
关闭且缓冲排空后。
代码:1 true 0 false。
工程:WaitGroup 等所有发送者结束后由协调者 close;或每个 worker 独立 channel 再 fan-in。
延伸阅读与资料来源
| 资料 | 类型 | 支撑 |
|---|---|---|
| Spec — Channel types | 规范 | 类型与操作 |
| Spec — Close | 规范 | close 语义 |
| Memory Model — Channel | 规范 | 同步 |
| Effective Go — Channels | 文档 | 用法风格 |
| Share Memory By Communicating | 博客 | 设计示例 |
笔记元信息
- 建议文件名:
go-channels.md - 所属阶段:阶段四
- 本篇状态:已深化
- 建议下一篇:Go select