三类常见并发bug
| Bug 类型 | 一句话描述 |
|---|---|
| Data Race | 多人同时抢一块数据,结果乱套 |
| Deadlock | 互相等对方,大家一起卡死 |
| Goroutine 泄漏 | 开了一堆 goroutine,但忘了关,内存越跑越大 |
一、Data Race
典型问题代码:
Go
var n int // 全局变量,初始值为0
func add() {
for i := 0; i < 1000; i++ {
n++ // 对全局变量n自增1
}
}
func main() {
go add() // 启动第一个goroutine执行add()
go add() // 启动第二个goroutine执行add()
time.Sleep(100 * time.Millisecond) // 主线程休眠,等待两个goroutine执行完
fmt.Println("n =", n) // 打印最终结果
}
- 会出现的问题:按理说会输出2000(两个goroutine每个都写入1000),但是每次运行程序,输出的值都不一样且达不到2000,可能是1878也可能是1923...
- 为什么会出现这样的问题:两个 goroutine 同时执行
n++,互相覆盖对方写入的操作 - n++实际覆盖三步操作:读,改,写(不符合原子性),因此在写的过程中两个goroutine会互相干扰
这个案例就体现了data race的典型问题:结果不确定
如何看到底哪行代码出现问题,以及哪两个goroutine出现了冲突:
使用:-race
go run -race main.go

race的局限性:
| 能发现 | 不能发现 |
|---|---|
| 并发读写同一块内存 | 死锁 |
| goroutine 泄漏 | |
| 逻辑顺序错误(没有写冲突) |
如何解决data race问题:加Mutex以及WaitGroup
重点:用 mu.Lock() 和 mu.Unlock() 保护 n++
Go
var (
n int
mu sync.Mutex // 变量声明:互斥锁
wg sync.WaitGroup // 变量声明:WaitGroup
)
func add() {
defer wg.Done()
for i := 0; i < 1000; i++ {
mu.Lock() // 加互斥锁
n++
mu.Unlock() // 解锁
}
}
func main() {
wg.Add(2) // 要等两个goroutine
go add()
go add()
wg.Wait() // 阻塞主线程,直到WaitGroup的计数器减到0
fmt.Println("n =", n) // 稳定输出 2000
}
二、死锁Dead Lock
典型问题代码:
Go
func main() {
ch := make(chan int)
ch <- 1 // 发送:等有人接收
fmt.Println(<-ch) // 接收:等有人发送
}
- 会出现的问题:死锁

- 为什么会出现这个问题:无缓冲channel需要一个goroutine发送,一个goroutine同时接收;而这里只有一个goroutine,会卡在 ch <- 1这一步
- 无缓冲 channel:发送方和接收方必须同时准备好
修复代码:
Go
func main() {
ch := make(chan int)
go func() {
ch <- 1 // 另一个人发
}()
fmt.Println(<-ch) // main 收
}
三、Goroutine泄露
典型问题代码:
Go
func leak() {
ch := make(chan int)
go func() {
val := <-ch // 永远等不到数据
fmt.Println(val)
}()
// 函数返回,ch 没人再往里发
// 但 goroutine 还在等,永远不会退出
}
- 问题:只有 val := 在收集channel的数据,但是没有人发给他,导致goroutine一直在等待
- 主要的问题:无缓冲通道的接收操作没有对应的发送操作
| 症状 | 说明 |
|---|---|
| 内存越来越大 | 堆积的 goroutine 占内存 |
| 没有报错 | 不像死锁那样显式崩溃 |
| goroutine 数量只增不减 | 最隐蔽的问题 |
如何发现 Goroutine 泄露

修复代码:加推出信号
Go
func noLeak() {
ch := make(chan int)
quit := make(chan struct{}) // 退出信号 channel
go func() {
select {
case val := <-ch:
fmt.Println(val)
case <-quit: // 收到退出信号就走
return
}
}()
close(quit) // 通知 goroutine 退出
}
四、总结

实用习惯:
- 测试时始终带 -race:go test -race -count=1 ./...
- 给每个等待点加日志,知道「谁在等谁」
- 怀疑死锁:按 Ctrl+\ 打印所有 goroutine 当前的调用
- 不要用 time.Sleep 假装同步
- 先让程序跑正确,再谈性能
先判断bug类型,再挑选工具