死锁发生时应立即查看 panic 日志末尾的 goroutine 列表,而非重启程序;goland 无法静态检测死锁,运行时 panic 是唯一确认依据。

看到 fatal error: all goroutines are asleep - deadlock! 就该停手看 panic 日志
GoLand 本身不提供“死锁静态检测”功能,它不会在编码时标红或预警潜在死锁。真正能确认死锁的,只有运行时 panic 输出的那句错误——它不是提示,是终局判决。一旦出现,程序已退出,GoLand 的调试器也进不去。所以别指望 IDE 主动发现,要盯紧控制台输出。
关键动作是:立刻翻 panic 日志末尾的 goroutine 列表,而不是在 GoLand 的 “Run” 窗口里点“Stop”再重跑。常见误操作是 panic 后直接关掉终端、清空日志、改代码再 run —— 这等于丢掉唯一线索。
- 主 goroutine 卡在
ch 或 <code>?说明 channel 收发没配对,大概率缺接收 goroutine 或发送 goroutine - 卡在
wg.Wait()?检查wg.Add()是否在 goroutine 启动前调用,以及所有路径是否都执行了wg.Done() - 卡在
mu.Lock()?重点查是否同一个 goroutine 重复调用Lock(),或 deferUnlock()写错位置
用 GODEBUG=schedtrace=1000 快速确认是否真全员阻塞
在 GoLand 的 Run Configuration → Environment variables 里加一行:GODEBUG=schedtrace=1000,然后运行。Go 运行时每秒会打印一行调度摘要,例如:
SCHED 0ms: gomaxprocs=8 idle=7 running=1 grunning=0 ngs=16
真正要盯的是末尾的 ngs=16(goroutines 数)和 running=1。如果连续几秒都是 ngs=1 且 running=0、grunning=0,说明只剩 main goroutine,其他全卡死或已退出——基本可断定是死锁,不是慢。
GoLand 2026.1.1 是 2026.1 发布后的首个维护修正版本,适合已经开始体验 2026.1 新功能并希望同步补丁的开发者。它更适合用于入门项目、现有项目迁移测试和 IDE 行为验证。
- 别写成
GODEDEBUG,拼错变量名就无效 - 这个输出会刷屏,建议在 GoLand 的 Run 窗口右键 → “Fold Lines That Contain” → 输入
SCHED折叠无关日志,只留调度行 - 若看到
waiting=5长期不变,而没有对应 goroutine 在runtime.chansend或runtime.gopark外执行,就是典型 channel 配对失败
在 GoLand 里启用 /debug/pprof 要提前埋点,不能等卡住再加
死锁发生时 HTTP server 根本没机会响应请求,所以 /debug/pprof/goroutine?debug=2 在 panic 后完全失效。想用它,必须在代码启动前就导入并起服务:
import _ "net/http/pprof"
<p>func main() {
go http.ListenAndServe("localhost:6060", nil)
// 其余逻辑...
}</p>
然后在 GoLand 的 Run Configuration → Program arguments 里加 -gcflags="-l"(可选,避免内联干扰堆栈),运行后手动访问 http://localhost:6060/debug/pprof/goroutine?debug=2。
- 如果程序没 panic 但长时间无响应,才轮到 pprof 上场;这时在 GoLand 的 “Services” 工具窗口点 + → HTTP Client,粘贴 URL 请求即可
- 重点关注输出里重复出现的调用点:
runtime.chansend、sync.(*Mutex).Lock、runtime.gopark - 别信
/debug/pprof/block:它统计“耗时阻塞”,而死锁是“永久阻塞”,往往一秒就 panic,block profile 压根来不及采集
别依赖 GoLand 的 debugger 单步过 ch
无缓冲 channel 的发送操作是原子阻塞点:一旦执行 ch ,当前 goroutine 就彻底停住,Debugger 会卡在那一行不动,无法继续 Step Over 或 Step Into。这不是 IDE 卡了,是 runtime 真的挂了。
此时强行点 “Resume Program” 没用,因为没人唤醒它;设断点在下一行也永远触发不了。你只能靠 panic 日志或 schedtrace 判断问题出在哪儿,而不是靠 Debugger 走完逻辑。
- 带缓冲 channel(如
make(chan int, 1))可以撑一次发送,Debugger 能走过去,但第二次ch 仍会卡死——别被第一次成功迷惑 - 用
select { case ch 包一层再调试,至少能让 Debugger 继续走,但 default 分支不能空转,否则掩盖真实阻塞 - 最可靠的调试方式,其实是把 channel 操作拆成两步:先用
len(ch) == cap(ch)(仅限有缓冲)或select { case 探测可写性,再发;这样 Debugger 才有落脚点










