Skip to content

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 是独立堆 + 消息传递,不共享内存。

底层机制 ​

1. goroutine 不是裸线程 ​

goroutine 特点:

  • 创建成本比 OS 线程低很多
  • 初始栈很小,可按需增长
  • 由 Go runtime 调度到少量 OS 线程上执行

所以:

  • 开很多 goroutine 通常可行
  • 但“很多”不等于“无限免费”
  • 如果阻塞、泄漏、背压没设计好,依然会出问题

2. channel 同时承担“传值”和“同步” ​

最小示例:

go
ch := make(chan int)

go func() {
    ch <- 42
}()

value := <-ch
fmt.Println(value)

可能执行顺序

  1. 主 goroutine 创建一个无缓冲 channel
  2. 启动子 goroutine,准备向 ch 发送 42
  3. 主 goroutine 执行 <-ch,若此时还没收到值会阻塞等待
  4. 子 goroutine 执行 ch <- 42,与接收方完成一次同步交接
  5. 主 goroutine 拿到值后继续执行,输出 42

可能输出

text
42

预期现象

  • 这个例子最值得观察的不是“打印了 42”,而是发送和接收必须彼此对齐
  • 如果去掉接收方或发送方,程序就可能一直阻塞,最后触发 all goroutines are asleep - deadlock!

观察重点

  • 无缓冲 channel 既传值,也做同步握手
  • goroutine 是并发启动了,但真正推进仍受发送/接收时序约束

这里 channel 做了两件事:

  1. 传递值 42
  2. 保证发送与接收在时序上对齐

无缓冲 channel ​

特点:

  • 发送方必须等接收方
  • 非常适合同步交接

有缓冲 channel ​

特点:

  • 缓冲未满时发送不必立刻等接收方
  • 更适合生产消费解耦

但缓冲不是越大越好,过大常常只是把问题堆起来。

3. context 负责取消与超时传播 ​

最典型写法:

go
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

可能执行顺序

  1. 基于 context.Background() 创建一个带 2 秒超时的子 context
  2. runtime 启动对应的超时计时器
  3. 调用方把 ctx 继续传给下游 goroutine / I/O / RPC
  4. 若任务先完成,defer cancel() 在返回时主动释放资源
  5. 若 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 对照 ​

维度GoKotlin / AndroidDart / FlutterJS / Web
轻量任务单元goroutinecoroutineFuture / isolatePromise / async task
数据通信channelChannel / Flow / SharedFlowStream / message passingPromise / event / Worker message
取消传播contextJob / CoroutineScope手动控制 / isolate 协议AbortController / 自定义协议
真正并行runtime 调度到多线程Dispatcher 调度到多线程isolateWorker

常见场景 ​

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/

复习检查题 ​

  1. 为什么说 goroutine 不是“更便宜的线程”这么简单?

    答:因为它不仅更轻量,还依赖 Go runtime 调度到少量 OS 线程上运行,重点在于调度模型与工程治理,而不只是创建成本低。

  2. channel 为什么不只是“队列”?

    答:因为它不仅传值,还负责发送与接收方之间的同步时序;无缓冲 channel 尤其强调这种同步交接语义。

  3. context 最该解决什么问题?

    答:取消、超时、截止时间和链路控制传播;它不是普通业务参数容器。

速记 ​

  • goroutine:轻量任务,不是白送线程
  • channel:传值 + 同步
  • context:取消 + 超时 + deadline
  • Go 并发难点不在启动,在停得干净

站点构建时间:2026/8/24 23:43:17