智汇百科
霓虹主题四 · 更硬核的阅读氛围

Go并发编程常见问题及解决方案

发布时间:2025-12-15 21:48:22 阅读:55 次

并发不是万能钥匙

很多人刚学会 goroutine,看到任何任务都想着开协程处理。比如有个小服务要读取几个配置文件,非得每个文件开一个 goroutine 去读。其实文件不大、数量不多的时候,串行读完可能还更快,毕竟调度也有开销。别把并发当银弹,用错了地方反而拖慢程序。

忘记关闭 channel 引发死锁

channel 是 Goroutine 通信的好工具,但容易犯的错是只关不收或只收不关。比如你在主 goroutine 关了 channel,但子 goroutine 还在往里写,程序直接 panic。反过来,如果没人关 channel,range 循环会一直等下去,造成死锁。

ch := make(chan int, 3)
ch <- 1
ch <- 2
close(ch)
for v := range ch {
fmt.Println(v)
}

记得:只有发送方负责 close,接收方只管读就行。

竞态条件:看似正常的代码出怪事

多个 goroutine 同时读写同一个变量,没加保护,结果数据对不上。比如统计用户请求次数,每来一个请求就 count++,跑着跑着发现数字比实际请求数少。这就是典型的竞态(race condition)。

var count int
for i := 0; i < 1000; i++ {
go func() {
count++ // 危险操作
}()
}

解决办法要么用 sync.Mutex,要么换 sync/atomic 包里的原子操作,比如 atomic.AddInt64。

goroutine 泄露:悄悄吃光内存

开了 goroutine 却没给退出机会,时间一长堆积如山。比如一个后台任务监听 channel,但主程序已经不需要它了,channel 却再也收不到信号,这个 goroutine 就卡死了。

常见做法是配合 context 使用。传 context 到 goroutine 里,主程序取消时,子任务能感知并退出。

ctx, cancel := context.WithCancel(context.Background())
go func(ctx context.Context) {
for {
select {
case <-time.After(time.Second):
fmt.Println("working...")
case <-ctx.Done():
fmt.Println("stopped")
return
}
}
}(ctx)
// 某个时刻调用 cancel()
cancel()

误用 WaitGroup 导致阻塞

WaitGroup 用来等一组 goroutine 结束,但容易数错数量。比如启动了 3 个任务,却只 Add(2),那最后一个 Done 可能让 Wait 一直卡住。或者在 goroutine 外部执行 Done,也会 panic。

稳妥的做法是在 goroutine 内部 defer wg.Done(),确保一定会被调用。

var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Printf("task %d done\n", i)
}()
}
wg.Wait()

共享变量传递方式不对

新手常犯的一个错是把循环变量直接传进 goroutine,结果所有协程用的都是同一个值。比如下面这段代码,输出全是 3。

for i := 0; i < 3; i++ {
go func() {
fmt.Println(i) // 全部输出 3
}()
}

解决方法是把变量作为参数传进去:

for i := 0; i < 3; i++ {
go func(val int) {
fmt.Println(val)
}(i)
}