Go 并发编程的 5 个常见坑,新手先看

我带过一个实习生,他写的第一段并发代码里,循环变量直接进了 goroutine 闭包。跑出来的结果每次都不一样,他盯着屏幕看了半小时没想通。Go 并发编程新手最容易踩哪些坑,八成集中在这五个里,提前知道能省好几个通宵。

一、Go 并发编程新手最容易踩哪些坑:循环变量被闭包抓走

现象是循环里起了几个 goroutine,打印出来的下标全是同一个数,或者干脆是最后一个。

原因在于闭包捕获的是那个变量本身,不是它当时的取值。循环跑得比 goroutine 启动快,等 goroutine 真正执行时,变量已经走到末尾了。

怎么避:把当前值当成参数传进 goroutine,或者在循环体里重新声明一个局部变量,让每次迭代有自己的那份。

具体写法随语言版本略有差异,以你所用版本的官方说明为准——这一点上老教程和新版本的行为不一样,直接抄博客容易踩第二次。

二、goroutine 起了没人管

现象是程序跑着跑着越来越慢,或者干脆卡死不退出。

原因是有 goroutine 在等一个永远不会有值的 channel。往无缓冲 channel 发数据,如果没人接,发送方就一直阻塞。

怎么避:每一个起 goroutine 的地方,都要能回答「它什么时候结束」。答不上来的,就是隐患。写完一段并发代码,回头数一遍起了几个、收了几个。

三、以为加了锁就安全

现象是锁也加了,数据偶尔还是不对。

原因通常是读的时候忘了加锁,或者读出来的值是内部数据的引用,在锁外面被改了。

怎么避:锁和它保护的数据绑在一起,包成一个小结构体,所有访问都走方法。别让调用方有机会绕过锁直接碰字段。

四、超时和取消没往下传

现象是上游早就断了,下游还在跑,资源一直占着。

原因是调用链中间某一层用了新的空 context,把上游的取消信号截断了。

怎么避:context 从入口造一次,之后只传不造。函数第一个参数接 ctx,一路往下透传到底。中间层除非要调特别慢的外部依赖,否则不要自己再包超时。

五、并发开得越多越快

现象是把并发数从一小批提到一大批之后,整体耗时没降,反而升了。

原因在 Go 里通常是 goroutine 本身很便宜,但你下游的资源不便宜——数据库连接池有上限、对方接口有速率限制、磁盘就那点吞吐。goroutine 堆在等这些资源,只会让排队更长、内存更高。

怎么避:给并发加一个上限。用带缓冲的 channel 当令牌桶,或者只在真的需要限流时用信号量模式。先测出下游能承受多少,再定这个数。

六、先修哪个,以及一个习惯

如果五个都占了,我建议按这个顺序。

先修坑二和坑四,因为它们会让程序行为不可预测——卡死和资源泄漏在线上最难处理。

再修坑一,它是纯粹的错,改起来最快,但影响的是所有并发代码的正确性。

坑三需要一次全量排查,因为漏掉的地方不一定有明显症状,可以配合 -race 跑一轮找。

坑五不着急,通常等你有性能压力了才会撞上。

我现在的做法是:写并发代码前先画一下数据流,标出哪份数据被谁碰、goroutine 在哪里结束、取消信号走哪条路。

画不出来就说明还没想清楚,这时候直接写代码,上面五个坑至少会踩三个。所以 Go 并发编程新手最容易踩哪些坑,其实就取决于开工前有没有把这三个问题答清楚。

常见问题

Q:Go 并发编程新手最容易踩哪些坑?

A:循环变量被闭包抓走、goroutine 起了没人收、以为加了锁就安全、取消信号没传下去、并发数开太大。其中前两个最常见。

Q:怎么快速发现自己代码里有并发问题?

A:跑一遍带 -race 的测试,它能查出大部分数据竞争。剩下的靠数 goroutine 的收尾情况。

Q:并发数设多少合适?

A:没有通用数字,取决于下游资源能承受多少。先小后大,边压边看,别一开始就拉满。

相关阅读


本文作者西米,37 岁程序员,一个人加五个 AI 员工在跑一家公司。踩过的坑和真实账本都写在公众号「西米分享」。

完整代码、踩坑清单与最新进展,都在公众号。

📱 关注公众号「西米分享」