Goroutine 死锁在测试中可能沉默,在线上可能致命。本文从一个真实死锁案例出发 ,结合 pprof
、runtime.Stack
、goleak
、-race
等工具,梳理一套实战级调试流程,解决下面这个典型场景:
💥「服务随机卡住、无错误日志、CPU占用不高」
1. 现场还原:最小死锁复现样例
go
func main() {
ch := make(chan int)
go func() {
fmt.Println("子协程准备发送")
ch <- 1
fmt.Println("子协程发送成功")
}()
time.Sleep(time.Second)
// 忘了接收:<-ch
}
- 现象:无输出,程序卡住,无 panic。
- 实质:子协程阻塞在 ch <- 1,主协程未消费,造成死锁。
2. runtime.Stack 查看所有 Goroutine 状态
go
import "runtime"
func dumpGoroutines() {
buf := make([]byte, 1<<20)
runtime.Stack(buf, true)
fmt.Printf("=== GOROUTINE DUMP ===\n%s\n", buf)
}
输出示例:
less
goroutine 1 [sleep]:
main.main()
/main.go:11
goroutine 2 [chan send]:
main.main.func1()
/main.go:7
👉 结论:Goroutine 2 卡在发送,主协程没接收。
3. 使用 goleak 检测 Goroutine 泄露(推荐测试期使用)
go
测试文件中加入:
import (
"testing"
"go.uber.org/goleak"
)
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}
执行:
bash
go test -v ./...
如有泄露,输出:
go
Found leaked goroutine: goroutine 4 [chan send]:
✅ 建议集成在 CI 流水线中防止问题上线。
4. 使用 pprof 分析线上卡死服务(服务未 panic)
在你的服务中启用:
go
import _ "net/http/pprof"
func init() {
go http.ListenAndServe(":6060", nil)
}
运行后,访问:
bash
curl http://localhost:6060/debug/pprof/goroutine?debug=2 > goroutines.txt
分析 goroutines.txt:
css
goroutine 33 [chan receive]:
<-chan blocking in worker()
👀 直接定位卡在哪个函数的哪个 channel。
5. 使用 -race 检测数据竞争(可能是死锁根因)
go
go run -race main.go
输出示例:
vbnet
WARNING: DATA RACE
Write at 0x00c0000a40 by goroutine 6:
- 虽然不是死锁检测器,但很多隐蔽死锁都是数据竞争引起的资源状态异常。
🧠 6. 实战建议清单
场景 | 推荐方式 |
---|---|
✅ 本地调试死锁 | runtime.Stack() |
✅ 单测中检查泄露 | goleak.VerifyTestMain |
✅ 服务无响应但无 panic | 启用 pprof 并分析 goroutine dump |
✅ 发布前校验并发问题 | 使用 go test -race ./... |
✅ 查锁顺序问题 | 统一封装锁顺序,避免交叉锁 |
7.死锁常见陷阱总结
markdown
1. ❌ channel 写入无人接收(无缓冲)
2. ❌ WaitGroup.Add 与 Done 不匹配
3. ❌ 多把锁交叉持有(锁顺序不一致)
4. ❌ select 中只有写分支,没有 default
5. ❌ chan 写入后未关闭,协程永久阻塞
8. 推荐写法:select + context 控制阻塞
✅ select 限时写入
lua
select {
case taskChan <- task:
case <-time.After(2 * time.Second):
log.Println("任务通道阻塞,已超时")
}
✅ 使用 context 控协程退出
css
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
select {
case <-ctx.Done():
log.Println("协程超时退出")
}
9. Pre-Push 自动检测脚本(可集成 Git Hook)
bash
#!/bin/bash
set -euo pipefail
go test -race ./...
go vet ./...
staticcheck ./...
echo "✅ 死锁/竞态/静态检查通过,允许推送"
将脚本放入 .git/hooks/pre-push,每次提交前自动检查。