Appearance
Go goroutine / channel / context
Go 的并发核心不是“自己手动管线程”,而是 goroutine 作为轻量并发任务、channel 作为通信同步原语、context 作为取消与超时治理边界。
一句话定义
- goroutine:比线程轻得多的并发执行单元
- channel:在 goroutine 之间安全传递数据与同步时序
- context:把取消、超时、截止时间和链路元信息向下传递
三者组合起来,构成 Go 最常见的并发工程模型。
为什么需要
Go 在服务端和基础设施里很常见,但很多人只停在:
- 会写
go f() - 会
chan int - 会
select
真正困难的是工程语义:
- 什么时候 channel 是同步手段,什么时候只是传数据?
- 一句话答:无缓冲 channel 会阻塞收发双方、强制同步交接,本身就是同步手段;有缓冲 channel 偏向传数据队列;关键是“是否依赖发送方与接收方同时在场”。
- 为什么 goroutine 泄漏会长期吃内存和调度资源?
- 一句话答:goroutine 没有自动取消机制,泄漏的 goroutine 不会退出回收,长期占着栈内存并持续参与调度,积累后内存与调度开销不断上涨。
- 为什么 context 不该用来传业务参数?
- 一句话答:
context的定位是取消 / 超时 / 截止时间 / 链路控制传播;往里塞业务参数会让跨层依赖隐式化、难以测试和维护。
- 一句话答:
- Go 的 goroutine 和 Kotlin 协程、Dart isolate 到底怎么对照?
- 一句话答:goroutine 由 runtime 调度到少量 OS 线程上、共享内存,类似协程但缺少结构化取消;Kotlin 协程有
Job/ 结构化并发治理;Dart isolate 是独立堆 + 消息传递,不共享内存。
- 一句话答:goroutine 由 runtime 调度到少量 OS 线程上、共享内存,类似协程但缺少结构化取消;Kotlin 协程有
底层机制
1. goroutine 不是裸线程
goroutine 特点:
- 创建成本比 OS 线程低很多
- 初始栈很小,可按需增长
- 由 Go runtime 调度到少量 OS 线程上执行
所以:
- 开很多 goroutine 通常可行
- 但“很多”不等于“无限免费”
- 如果阻塞、泄漏、背压没设计好,依然会出问题
2. channel 同时承担“传值”和“同步”
最小示例:
go
ch := make(chan int)
go func() {
ch <- 42
}()
value := <-ch
fmt.Println(value)可能执行顺序
- 主 goroutine 创建一个无缓冲
channel - 启动子 goroutine,准备向
ch发送42 - 主 goroutine 执行
<-ch,若此时还没收到值会阻塞等待 - 子 goroutine 执行
ch <- 42,与接收方完成一次同步交接 - 主 goroutine 拿到值后继续执行,输出
42
可能输出
text
42预期现象
- 这个例子最值得观察的不是“打印了 42”,而是发送和接收必须彼此对齐
- 如果去掉接收方或发送方,程序就可能一直阻塞,最后触发
all goroutines are asleep - deadlock!
观察重点
- 无缓冲 channel 既传值,也做同步握手
- goroutine 是并发启动了,但真正推进仍受发送/接收时序约束
这里 channel 做了两件事:
- 传递值
42 - 保证发送与接收在时序上对齐
无缓冲 channel
特点:
- 发送方必须等接收方
- 非常适合同步交接
有缓冲 channel
特点:
- 缓冲未满时发送不必立刻等接收方
- 更适合生产消费解耦
但缓冲不是越大越好,过大常常只是把问题堆起来。
3. context 负责取消与超时传播
最典型写法:
go
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()可能执行顺序
- 基于
context.Background()创建一个带 2 秒超时的子context - runtime 启动对应的超时计时器
- 调用方把
ctx继续传给下游 goroutine / I/O / RPC - 若任务先完成,
defer cancel()在返回时主动释放资源 - 若 2 秒先到,
ctx.Done()会被关闭,下游应尽快感知并退出
可能输出 / 外在表现
- 这段初始化代码本身通常没有直接输出
- 在完整示例里,更常见的是收到
context deadline exceeded、ctx.Err()变为非空,或日志里出现超时取消信息
预期现象
context真正建立的是一条“取消/超时传播链路”- 只创建
ctx还不够,下游必须显式监听Done()或把ctx传给支持取消的 API
观察重点
defer cancel()不只是语法习惯,它对应 timer 与下游资源的及时回收context的价值在于让多个 goroutine / 调用层共享同一退出信号
下游收到 ctx 后应:
- 监听
ctx.Done() - 在超时/取消后尽快退出
- 把取消继续向更深层传递
这和 Kotlin 的 Job.cancel() / 协程结构化取消在目标上很像:
- 不是只关心“任务启动”
- 更关心“如何正确停下来”
Android / Flutter / Web / Backend 对照
| 维度 | Go | Kotlin / Android | Dart / Flutter | JS / Web |
|---|---|---|---|---|
| 轻量任务单元 | goroutine | coroutine | Future / isolate | Promise / async task |
| 数据通信 | channel | Channel / Flow / SharedFlow | Stream / message passing | Promise / event / Worker message |
| 取消传播 | context | Job / CoroutineScope | 手动控制 / isolate 协议 | AbortController / 自定义协议 |
| 真正并行 | runtime 调度到多线程 | Dispatcher 调度到多线程 | isolate | Worker |
常见场景
1. 服务端并发 fan-out 请求
比如:
- 同时打多个下游服务
- 汇总结果
- 某个超时就整体取消
Go 里常见做法是:
- 多 goroutine 并发
- channel 收集结果
- context 控制超时/取消
2. 管道式处理
例如:
- 读数据
- 解析
- 过滤
- 写出
每段一个 goroutine / 一组 worker,用 channel 串起来,是 Go 很经典的并发组织方式。
3. goroutine 泄漏
最常见问题之一:
- 上游已经不关心结果了
- 下游 goroutine 还卡在发送/接收/阻塞 IO
- 长期占资源
这和 Kotlin 协程没绑生命周期、Flutter isolate 无回收协议,本质上是同类问题。
常见坑
| 坑 | 现象 | 修法 |
|---|---|---|
只会 go f() 不做回收 | goroutine 泄漏 | 用 context / close / 明确退出协议 |
| channel 过大当缓存池 | 延迟问题被隐藏 | 缓冲大小围绕背压设计 |
| 用 context 传业务参数 | 语义混乱 | context 只放链路控制与少量元信息 |
忘记 cancel() | 资源不及时释放 | defer cancel() 成为习惯 |
与相近概念对比
| 概念 | 本质 | 适合场景 |
|---|---|---|
| goroutine | 轻量并发执行单元 | 并发任务拆分 |
| channel | 数据传递 + 同步 | worker pool / pipeline / fan-in/fan-out |
| context | 取消 / 超时 / deadline | 请求链路治理 |
| Kotlin coroutine | 可挂起任务 | 多线程调度下的结构化异步 |
| Dart isolate | 隔离内存单元 | CPU 密集任务隔离 |
对应实验
目前本主题先以理论文档为主,后续建议补:
labs/go/runtime-concurrency/goroutine-channel-basic/labs/go/runtime-concurrency/context-timeout-cancel/labs/go/runtime-concurrency/worker-pool/
复习检查题
为什么说 goroutine 不是“更便宜的线程”这么简单?
答:因为它不仅更轻量,还依赖 Go runtime 调度到少量 OS 线程上运行,重点在于调度模型与工程治理,而不只是创建成本低。
channel 为什么不只是“队列”?
答:因为它不仅传值,还负责发送与接收方之间的同步时序;无缓冲 channel 尤其强调这种同步交接语义。
context 最该解决什么问题?
答:取消、超时、截止时间和链路控制传播;它不是普通业务参数容器。
速记
- goroutine:轻量任务,不是白送线程
- channel:传值 + 同步
- context:取消 + 超时 + deadline
- Go 并发难点不在启动,在停得干净