写Go代码的时候,很多人一开始都觉得goroutine真香,一个go关键字就起一个协程,轻松实现并发。可实际用起来才发现,看似简单的背后藏着不少坑。
共享变量引发的数据竞争
就像家里两个孩子同时抢一个玩具,程序里多个goroutine同时读写同一个变量,结果谁也说不准。比如下面这段代码:
var count = 0
func main() {
for i := 0; i < 10; i++ {
go func() {
for j := 0; j < 100000; j++ {
count++
}
}()
}
time.Sleep(time.Second)
fmt.Println(count)
}你可能以为输出是100万,但实际往往不是。因为count++不是原子操作,多个goroutine同时改,数据就乱了。这时候得靠互斥锁来协调:
var mu sync.Mutex
go func() {
for j := 0; j < 100000; j++ {
mu.Lock()
count++
mu.Unlock()
}
}goroutine泄露:忘了收场
有时候启了一个goroutine去做事,结果任务早就结束了,但它还在那里挂着,像忘了关的水龙头。比如从channel读数据,但没人往里写:
ch := make(chan int)
go func() {
val := <-ch
fmt.Println(val)
}()
// 忘了close(ch)或写入数据
time.Sleep(time.Second)这个goroutine永远卡在那,内存就一点点被吃掉。解决办法是设置超时或者确保channel有写入和关闭。
select的随机性让人懵
select用来处理多个channel操作,但它的执行顺序是随机的。比如:
ch1, ch2 := make(chan int), make(chan int)
go func() { ch1 <- 1 }()
go func() { ch2 <- 2 }()
select {
case <-ch1:
fmt.Println("来自ch1")
case <-ch2:
fmt.Println("来自ch2")
}你不能指望每次都先打印ch1,因为它可能是ch2。这就像两个孩子同时喊你,你本能地回应那个声音更明显的,而不是按顺序来的。
WaitGroup使用不当导致死锁
WaitGroup常用来等所有goroutine结束,但如果Add和Done不匹配,程序就会卡住。比如:
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// 做点事
}()
}
wg.Wait()看着没问题,但如果Add放在了goroutine里面,就可能还没加上去,Wait就已经开始了,结果就是部分任务没被追踪,提前退出。必须保证Add在goroutine启动前完成。
过度依赖并发反而拖慢程序
不是所有任务都适合并发。就像让一个五岁孩子同时背三首诗,结果哪首都没记住。小任务开太多goroutine,调度开销反而比收益大。该串行时就别硬上并发。