mcpField
G
go-troubleshooting

go-troubleshooting

github.com/eduardo-sl开发83101 次安装2 周前更新
代码仓库

关于 go-troubleshooting

面向 Go 开发的故障排查技能,强调先复现和收集证据,再定位根因并修改代码。它按崩溃、死锁、泄漏、竞态、测试和构建等症状提供诊断步骤及参考资料。

主要功能

  1. 01先复现问题并记录证据
  2. 02按症状选择诊断流程
  3. 03使用 pprof 分析内存与 Goroutine
  4. 04排查竞态、间歇性测试和构建错误
  5. 05通过回归测试验证修复

适用场景

  1. 01根据 panic 堆栈定位 Go 代码问题
  2. 02调查服务卡顿、死锁或资源泄漏
  3. 03复现并排查间歇性测试失败
  4. 04处理 Go 模块或工具链构建错误

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/eduardo-sl/go-agent-skills --skill go-troubleshooting

安装后在 Claude Code 中输入/go-troubleshooting确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
common-bugs.md
flaky-and-build.md
methodology.md

详细介绍

Go 故障排查

先诊断,再修复。先复现、观察、定位,然后再改代码。 绝不要“修复”一个尚未解释清楚的症状——否则 Bug 只会转移到别处。

详细参考资料,按需加载:

  • references/methodology.md — 调试循环、先追踪调用方再归咎于代码、git bisect,以及自身推理中的危险信号。
  • references/flaky-and-build.md — 复现不稳定测试及其常见原因;构建、模块和工具链错误及其修复方法。
  • references/common-bugs.md — Go 中能顺利编译却行为异常的 Bug 的“症状 → Bug → 修复”对照表。

仅当下方摘要不足以解决问题时,才阅读参考文件。

0. 方法

  1. 说明预期结果与实际结果,并收集完整证据(完整堆栈、精确输入、跟踪 ID)。
  2. 在失败的测试中复现问题,然后缩小复现范围。
  3. 一个假设、一次更改、一次观察——绝不叠加多项更改。
  4. 修复原因,而非触发条件:不断追问错误值为何会存在。
  5. 三次修复失败,说明思维模型有误——从证据重新开始。如果问题根源早于你,git bisect run 可以找到引入问题的提交。

1. 根据症状选择排查流程

症状流程
结果错误,没有崩溃§0 方法,然后查看 references/common-bugs.md
崩溃并有堆栈跟踪§2 阅读 panic
程序挂起/请求停滞§3 转储 goroutine,找出阻塞点
fatal error: all goroutines are asleep§3 — Go 检测到完全死锁
内存持续增长,直到 OOM§4 堆配置文件差异
Goroutine 数量持续增长§5 Goroutine 配置文件差异
间歇性数据损坏/异常值§6 竞态检测器
需要交互式检查状态§7 Delve
生产环境中正在发生事故§11 重启前先采集信息
测试间歇性失败§9 不稳定测试
构建、模块或工具链错误§10 构建失败

2. 阅读 Panic

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x6bb0e4]

goroutine 43 [running]:
myapp/internal/service.(*UserService).Notify(0x0, {0xc000123456?, ...})
        /app/internal/service/user.go:87 +0x24
myapp/internal/handler.(*Handler).Create(0xc0001a2000, ...)
        /app/internal/handler/user.go:41 +0x1c5

按以下方式逐项读取:

  1. 第一行:panic 的类型。nil pointer dereference + addr=0x0 表示接收者、字段、map 或指针参数为 nil。
  2. 你自己的代码中的顶部栈帧:user.go:87 — 前往该位置。
  3. 栈帧中的接收者值:(*UserService).Notify(0x0, ...) — 第一个 参数 0x0 就是接收者:服务本身为 nil。 追踪它在哪里被构造(或为何未被构造)。
  4. goroutine 43 — 如果不是 goroutine 1,找到启动它的位置,并检查该处是否应该有 recover 边界。

3. 挂起与死锁

从挂起的进程获取 goroutine 转储:

kill -QUIT <pid>      # dumps all goroutine stacks to stderr, then exits
# or, if net/http/pprof is mounted:
curl 'localhost:6060/debug/pprof/goroutine?debug=2'

然后对栈进行分类:

  • sync.(*Mutex).Lock 上出现 [semacquire] — 找出哪个 goroutine 持有互斥锁:查找另一个位于临界区内的栈。两个 goroutine 各自持有一把锁 = 锁顺序反转。
  • [chan send] / [chan receive] — 通道另一端已经不存在。找到本应接收/发送的 goroutine,并查明它为何退出(或为何从未启动)。
  • [select] 中缺少 ctx.Done() 分支 — 阻塞调用忽略了取消信号。
  • 数百个相同的栈 — 这是泄漏(§5),不是死锁。

如果只是变慢而非卡住?启用 runtime.SetMutexProfileFraction / SetBlockProfileRate 并读取互斥锁和阻塞配置文件,或采集 5 秒跟踪 (/debug/pprof/trace?seconds=5),查看那些可运行却未运行的 goroutine。 kill -QUIT 会结束进程——在生产环境中,优先使用 pprof 端点。

4. 内存泄漏

在长期运行的服务中,将 net/http/pprof 挂载到私有监听器 (localhost:6060),绝不要挂载到公共 mux 上。

对比不同时段的堆配置文件——持续增长且从不回落就是泄漏:

curl -s localhost:6060/debug/pprof/heap > heap1.pb.gz
sleep 300   # let the leak accumulate
curl -s localhost:6060/debug/pprof/heap > heap2.pb.gz
go tool pprof -base heap1.pb.gz heap2.pb.gz
(pprof) top          # biggest positive delta = the leak
(pprof) list FuncName

常见原因:没有淘汰机制的无界缓存/map、固定住大型数组的子切片、从未停止的 time.Ticker、未关闭的响应体、不断增长的全局切片、遗忘的 goroutine 持有缓冲区。

5. Goroutine 泄漏

在 Go 1.27+ 中,先查看泄漏剖析数据——这些 goroutine 阻塞在通道或锁上,且永远不会有人将其释放:

go tool pprof http://localhost:6060/debug/pprof/goroutineleak

它无法检测通过全局变量存活的 goroutine,或仍在运行的 goroutine;对于这些情况,请对比两份 goroutine 剖析数据:

curl -s localhost:6060/debug/pprof/goroutine > g1.pb.gz
sleep 300
curl -s localhost:6060/debug/pprof/goroutine > g2.pb.gz
go tool pprof -base g1.pb.gz g2.pb.gz
(pprof) top    # the growing stack is your leak site

泄漏的调用栈会告诉你哪个 go 语句始终无法终止。 修复终止路径(context、关闭通道)——并发技能中的相关模式。在测试中,goleak(uber-go/goleak)会让遗留 goroutine 的测试失败。

6. 竞态检测器

go test -race ./...        # in CI, always
go build -race ./cmd/api   # staging binaries under real traffic

报告会展示两个调用栈:写入操作和并发的读取/写入操作,并标出各自 goroutine 的创建位置。解决方法绝不是“加个 sleep”——保护状态(互斥锁)、转移所有权(通道),或将其设为不可变。-race 只会报告实际执行过的竞态:一次干净的运行无法证明未经测试的路径没有问题。

7. Delve

dlv test ./internal/service -- -test.run TestTransfer   # debug a test
dlv attach <pid>                                        # running process
dlv core ./api core.1234                                # post-mortem

(dlv) break user.go:87
(dlv) continue
(dlv) print svc.repo          # inspect exact values
(dlv) goroutines -t           # all goroutines with stacks
(dlv) goroutine 43 bt         # switch and backtrace

需要查看实际值或 goroutine 状态,而不仅仅是位置时,请使用 delve。若要快速定位问题,添加一条有针对性的 t.Logf 或 slog.Debug,再运行一次测试,通常更快。

8. 诊断环境变量

GOTRACEBACK=all ./api        # panic dumps ALL goroutines, not just one
GODEBUG=gctrace=1 ./api      # GC cycles: pacing, heap goal, pause times
GOMEMLIMIT=512MiB ./api      # soft memory limit — mitigates OOM while
                             # you find the real leak
GOTRACEBACK=crash ./api      # also write a core dump, for `dlv core`
GODEBUG=schedtrace=1000 ./api  # scheduler state every second

Go 1.27+ 会在回溯中打印 pprof goroutine 标签;如果标签中包含敏感信息,请设置 GODEBUG=tracebacklabels=0。

9. 不稳定的测试

先确保测试能按需失败,再着手修改:使用 -count=200 -failfast,然后使用 -race、-shuffle=on(用打印出的种子重放),以及 -cpu=1,2,8。常见原因包括用 sleep 进行同步、使用真实时钟、共享包级状态、断言依赖 map 顺序、端口固定,以及先前测试遗留的 goroutine。在 CI 中重试只会让你看不到这个错误,用户却仍会遇到。

10. 构建失败

只看第一个错误——后续错误通常都是由它引起的。工具链版本过旧(GOTOOLCHAIN)、缺少 go.sum 条目、v1/v2 模块并存导致导入有歧义、操作系统专属文件、缺少 C 编译器、私有模块(GOPRIVATE),以及 Go 1.27 的 stdversion 检查,各自都有固定的解决方法。

命令以及完整的原因 → 修复对照表:references/flaky-and-build.md。

11. 生产事故

重启前先采集信息——重启会销毁证据:

  1. Goroutine 转储(/debug/pprof/goroutine?debug=2)、堆剖析数据、30 秒 CPU 剖析数据,以及 5 秒跟踪数据;如果服务运行了 flight recorder,也可以采集其快照(Go 1.25+)。
  2. 确切的构建信息:go version -m ./bin/api 会显示 Go 版本、模块版本和 VCS 修订版本。
  3. 然后重启,并按照上述流程离线分析采集到的数据。pprof 仅监听内部端口——它会暴露调用栈和命令行。

验证清单

  1. 在更改代码前复现症状(或通过转储/剖析数据捕获)
  2. 说明根本原因:能够解释为什么故障会在该位置发生
  3. panic 修复针对 nil/越界问题的根源,而不是使用包装器 recover
  4. 死锁修复建立统一的锁顺序,或移除共享锁
  5. 验证泄漏修复:修复后,goroutine/堆剖析数据保持平稳
  6. 并发相关修复后,go test -race ./... 通过
  7. 添加回归测试,确保没有修复时测试会失败
  8. pprof 端点仅绑定到 localhost/私有接口

评论

支持 Markdown