Go 并发精要
Go 的 goroutine 和 channel 因让并发显得简单而强大而受到赞誉,但许多工程师发现 channel 语义、取消以及关闭/清理模式出乎意料地棘手,并且容易产生泄漏或数据竞争。评论者将 Go 的模型与 Kotlin 协程、Java 虚拟线程、Erlang/Elixir 的 BEAM、Haskell 的 STM、Rust async 和 Julia 的任务进行比较,围绕人体工学、安全性,以及语言应当施加多少结构展开争论。一个反复出现的主题是:虽然 Go 的运行时和工具链(如 race detector)很强,但编写健壮的并发代码仍然需要谨慎设计、克制使用 channel,并且通常还要辅以 context 或 wait group 等模式。
Go 的并发模型
- 许多评论者称赞 goroutine 和 M:N 调度器,认为它们相比传统线程有一种“魔法般”的感觉,且异常流畅。
- 将抢占式绿色线程调度映射到 OS 线程上,被视为与协作式 async/await 模型相比的一项关键差异化特征。
- 也有人指出 Go 并不是这方面的首创(Erlang、Haskell),并认为它的模型更像是渐进演化而非革命。
Channel:强大与痛点
- Channel 和
select居于核心地位,但也被广泛描述为主要痛点。 - 围绕必须恰好关闭一次、重复关闭或向已关闭 channel 发送会 panic、以及从已关闭 channel 接收时返回零值等要求,被认为不直观且容易出错。
- 正确的清理、多个发送者模式,以及避免 goroutine 泄漏,都需要纪律性(context、waitgroup、对 channel 关闭的所有权要谨慎)。
- 一些有经验的用户表示 channel 使用过度;mutex、原子操作、waitgroup,以及简单的共享切片往往就足够了。
与其他语言的比较
- Kotlin:协程 + 结构化并发 +
trySend/tryReceive被认为比 Go 的 channel 更符合人体工学,也更安全。 - Haskell:STM、高层并发组合子,以及结构化组合(超时、竞态)被认为比 Go 以 channel 为中心的风格更强大,也更容易推理。
- Erlang/Elixir(BEAM):被奉为容错、基于 actor 的并发与监督体系的“局部最优”;Go 被看作更熟悉,但整体性较弱。
- Java:虚拟线程 + 结构化并发 + futures 被认为在很多工作负载下的人体工学更优,不过也有人指出 Java 来得更晚。
- Rust:带有无数据竞争保障的任务 + channel 受到称赞;有人表示这让回到 Go 变得困难。
- Julia 和 Zig 也被提到拥有强大、现代的并发故事。
工具、性能与内存
- Go 的 race detector 受到称赞,但其覆盖范围受限于测试实际执行到的内容;评论者希望能有更好的静态检查来验证 mutex/atomic 的正确性。
- 有人指出高压下的内存行为以及需要超配资源;也有人拿它与 Java 的调试体验做出正面比较。
- 还有几条评论强调,Go 在存在数据竞争时并不是完全内存安全的。
模式与最佳实践
- 建议:先写串行代码;只有在需要时再加入并发,而且很少通过 channel 来实现。
- 默认使用无缓冲 channel;只有在有明确理由时才使用缓冲。
- 优先使用 context 做取消与存活管理,并避免同时持有多个 mutex,以规避经典的死锁模式。