Go 中的上下文控制
Go 开发者在权衡如何负责任地使用 `context` 和 goroutine,重点关注取消、资源生命周期,以及避免在库中隐藏并发。许多人倾向于让 API 看起来是同步的,由调用者决定是否并发执行,同时也承认库内部 goroutine 和长时间运行的工作线程有其合理场景。讨论还批评 `context` 既承载取消又承载请求作用域数据的混合属性,将其与 Rust 和 C# 中的模式进行比较,并提到了结构化并发以及 Go“没有 async 着色”理想的局限。
库中的 Goroutine 与 API 设计
- 一个强烈的主题是:库 API 应该看起来是同步的;是否需要并发应由调用者自己通过启动 goroutine 来决定。
- 库内部的 goroutine 是可以接受的,前提是它们:
- 明确属于该抽象的一部分(例如爬虫、并行 map、批处理 pub/sub、指标刷新器)。
- 有界、文档完备且可调(限制、速率、批量处理、in-flight 上限)。
- 在公共函数返回之前被妥善清理(结构化并发风格,通常通过
errgroup或WaitGroup)。
- 反对“库里永远不要启动 goroutine”的人认为这种说法过于僵硬;对某些工作负载来说,让库负责调度会更简单也更安全。
- 后台 goroutine 中的 panic 是一个令人担忧的问题:除非调用者拥有 goroutine 边界,否则他们无法用
recover把它包住。
Context:目的、模式与痛点
- 主要有两个角色:
- 取消 / 截止时间 / “你现在可以停止了”。
- 请求作用域数据(日志器、链路追踪、认证等)。
- 许多人不喜欢它像个“装东西的袋子”这一点;很难知道为什么一个函数需要 context,或其中到底有什么。
- 另一些人则为显式的
ctx参数辩护:- 使可中断性和日志需求变得可见。
- 比隐式的线程局部存储更容易审查和推理。
- 讨论中的建议:
- 常见规则:不要存储 context;把它向下传递。
- 更细致地说:在本质上就是参数的 struct 中存储它是可以的;而在标准库中,为了向后兼容也有这种做法(例如
net/http),有时会使用NewRequestWithContext。 - 当需要存储“值但不要截止时间”时,
context.WithoutCancel被视为一个有用的折中方案。
- 摩擦点:
- 无法静态知道被调用者是否会遵守 context。
- 需要把
ctx一路传到“几乎每个函数”中的压力,尤其是在日志场景下。
Context 与函数着色 / 异步模型
- 一些人把
ctx看作一种温和的“着色”(有 context 的函数与没有 context 的函数),这在某种程度上削弱了 Go “没有 async 关键字” 的叙事。 - 另一些人则认为它远没有 async/await 那么受限:
- 你总可以使用
context.Background(),或者用超时对其进行包装。 - 它不会像其他语言那样制造严格的同步 / 异步分离。
- 你总可以使用
错误处理与零值(旁支话题)
- 围绕
(T, error)模式的争论:- 一派认为:即使
error被设置,零值 / nil 值也应该是“有用的”。 - 另一派认为:在大多数真实 API 中,零值没有意义或很危险;缺少 sum type 迫使人们采用笨拙的约定。
- 一派认为:即使