Skip to content

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 做了两件事:

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

无缓冲 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 对照

维度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 并发难点不在启动,在停得干净