Appearance
Go goroutine / channel / context
Go 的并发核心不是“自己手动管线程”,而是 goroutine 作为轻量并发任务、channel 作为通信同步原语、context 作为取消与超时治理边界。
一句话定义
- goroutine:比线程轻得多的并发执行单元
- channel:在 goroutine 之间安全传递数据与同步时序
- context:把取消、超时、截止时间和链路元信息向下传递
三者组合起来,构成 Go 最常见的并发工程模型。
为什么需要
Go 在服务端和基础设施里很常见,但很多人只停在:
- 会写
go f() - 会
chan int - 会
select
真正困难的是工程语义:
- 什么时候 channel 是同步手段,什么时候只是传数据?
- 为什么 goroutine 泄漏会长期吃内存和调度资源?
- 为什么 context 不该用来传业务参数?
- Go 的 goroutine 和 Kotlin 协程、Dart isolate 到底怎么对照?
底层机制
1. goroutine 不是裸线程
goroutine 特点:
- 创建成本比 OS 线程低很多
- 初始栈很小,可按需增长
- 由 Go runtime 调度到少量 OS 线程上执行
所以:
- 开很多 goroutine 通常可行
- 但“很多”不等于“无限免费”
- 如果阻塞、泄漏、背压没设计好,依然会出问题
2. channel 同时承担“传值”和“同步”
最小示例:
go
ch := make(chan int)
go func() {
ch <- 42
}()
value := <-ch
fmt.Println(value)这里 channel 做了两件事:
- 传递值
42 - 保证发送与接收在时序上对齐
无缓冲 channel
特点:
- 发送方必须等接收方
- 非常适合同步交接
有缓冲 channel
特点:
- 缓冲未满时发送不必立刻等接收方
- 更适合生产消费解耦
但缓冲不是越大越好,过大常常只是把问题堆起来。
3. context 负责取消与超时传播
最典型写法:
go
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()下游收到 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 并发难点不在启动,在停得干净