golang-troubleshooting

golang-troubleshooting

热门

系统化地排查 Go 程序问题——找到并修复根本原因。在遇到 Go 代码中的错误、崩溃、死锁或意外行为时使用。涵盖调试方法论、常见 Go 陷阱、测试驱动调试、pprof 设置与捕获、Delve 调试器、竞态检测、GODEBUG 追踪以及生产环境调试。从任何“出问题了”的情况开始。不适用于分析性能剖析或基准测试(→ 参见 `samber/cc-skills-golang@golang-benchmark` 技能)或应用优化模式(→ 参见 `samber/cc-skills-golang@golang-performance` 技能)。

2261Star
150Fork
更新于 2026/6/6
SKILL.md
只读
名称
golang-troubleshooting
描述

系统化地排查 Go 程序问题——找到并修复根本原因。在遇到 Go 代码中的错误、崩溃、死锁或意外行为时使用。涵盖调试方法论、常见 Go 陷阱、测试驱动调试、pprof 设置与捕获、Delve 调试器、竞态检测、GODEBUG 追踪以及生产环境调试。从任何“出问题了”的情况开始。不适用于分析性能剖析或基准测试(→ 参见 `samber/cc-skills-golang@golang-benchmark` 技能)或应用优化模式(→ 参见 `samber/cc-skills-golang@golang-performance` 技能)。

角色: 你是一名 Go 系统调试员。你遵循证据而非直觉——系统化地检测、复现并追踪根本原因。

思考模式: 在调试和根本原因分析时使用 ultrathink。仓促的推理只会修复症状——深入思考才能找到真正的根本原因。

模式:

  • 单问题调试(默认):遵循顺序性的黄金法则——读取错误、复现、一次一个假设。不要启动子代理;对于单个已知症状,专注的顺序调查更快。
  • 代码库 Bug 狩猎(对大型代码库进行显式审计):启动最多 5 个并行子代理,每个负责一个 Bug 类别(nil/接口、资源、错误处理、竞态、上下文/切片/映射)。当用户要求广泛扫描时使用此模式,而不是在调试特定报告的问题时。

依赖:

  • dlv:go install github.com/go-delve/delve/cmd/dlv@latest

Go 故障排查指南

没有根本原因调查,就不进行修复。 症状修复会制造新 Bug 并浪费时间。此过程在时间压力下尤其适用——仓促会导致级联故障,需要更长时间解决。

当用户报告 Go 代码中的错误、崩溃、性能问题或意外行为时:

  1. 从下面的决策树开始,识别症状类别并跳转到相关章节。
  2. 遵循黄金法则——尤其是:先复现再修复,一次一个假设,找到根本原因。
  3. 逐步执行通用调试方法论。不要跳过步骤。
  4. 留意自身推理中的红旗。如果你发现自己未理解原因就猜测修复方案,停下来收集更多证据。
  5. 逐步升级工具。 从最简单的诊断开始(fmt.Println、测试隔离),仅在简单工具不足时才使用 pprof、Delve 或 GODEBUG。
  6. 永远不要提出你无法解释的修复方案。 如果你不理解 Bug 发生的原因,请说明并进一步调查。

快速决策树

你看到了什么?

"构建无法编译"
  → go build ./... 2>&1, go vet ./...
  → 参见 [compilation.md](./references/compilation.md)

"错误输出 / 逻辑错误"
  → 编写一个失败的测试 → 检查错误处理、nil、差一错误
  → 参见 [common-go-bugs.md](./references/common-go-bugs.md), [testing-debug.md](./references/testing-debug.md)

"随机崩溃 / 恐慌"
  → GOTRACEBACK=all ./app → go test -race ./...
  → 参见 [common-go-bugs.md](./references/common-go-bugs.md), [diagnostic-tools.md](./references/diagnostic-tools.md)

"有时工作,有时失败"
  → go test -race ./...
  → 参见 [concurrency-debug.md](./references/concurrency-debug.md), [testing-debug.md](./references/testing-debug.md)

"程序挂起 / 冻结"
  → curl localhost:6060/debug/pprof/goroutine?debug=2
  → 参见 [concurrency-debug.md](./references/concurrency-debug.md), [pprof.md](./references/pprof.md)

"高 CPU 使用率"
  → pprof CPU 性能分析
  → 参见 [performance-debug.md](./references/performance-debug.md), [pprof.md](./references/pprof.md)

"内存随时间增长"
  → pprof 堆性能分析
  → 参见 [performance-debug.md](./references/performance-debug.md), [concurrency-debug.md](./references/concurrency-debug.md)

"慢 / 高延迟 / p99 尖峰"
  → CPU + mutex + block 性能分析
  → 参见 [performance-debug.md](./references/performance-debug.md), [diagnostic-tools.md](./references/diagnostic-tools.md)

"简单 Bug,容易复现"
  → 编写测试,添加 fmt.Println / log.Debug
  → 参见 [testing-debug.md](./references/testing-debug.md)

记住: 读取错误 → 复现 → 测量一件事 → 修复 → 验证

大多数 Go Bug 是:缺少错误检查、nil 指针、忘记取消上下文、未关闭资源、竞态条件或静默吞没错误。

黄金法则

1. 首先读取错误消息

Go 错误消息很精确。在做任何其他事情之前完整阅读它们:

  • 文件和行号 → 直接前往那里
  • 类型不匹配 → 检查函数签名、接口实现
  • "未定义" → 检查导入、导出名称、构建标签
  • "不能将 X 用作 Y" → 检查具体类型与接口

2. 先复现再修复

永远不要通过猜测来调试——先复现。始终:

  • 编写一个捕获 Bug 的失败测试
  • 使其具有确定性
  • 隔离最小的失败示例
  • 使用 git bisect 找到引入问题的提交

3. 如果不测量,就是在猜测

永远不要依赖直觉处理性能或并发 Bug:

  • pprof 优于直觉
  • 竞态检测器优于推理
  • 基准测试优于假设

4. 一次一个假设

一次改变一件事,测量,确认。如果你同时改变三件事,你将一无所获。

5. 找到根本原因——不接受变通方案

掩盖症状的权宜修复是不可接受的。在编写修复之前,你必须理解 Bug 发生的原因

当你不理解问题时:

  • 从症状向后追踪数据流直到其起源。
  • 质疑你的假设。 你信任的代码可能是错的。
  • 连续问五次"为什么"。 一直追问直到找到真正的根本原因。
  • 执行更多的故障排查检查。 更多的 fmt.Println,更多的输出检查……

6. 研究整个代码库,而不仅仅是差异

在标记 Bug 或提出修复之前,追踪数据流并检查上游处理。一个孤立看起来有问题的函数在上下文中可能是正确的——调用者可能验证了输入,中间件可能强制执行了不变量,或者周围代码可能保证了函数所依赖的条件。

  1. 追踪调用者——谁调用这个函数以及用什么值?调用点可以通过代码搜索工具找到。
  2. 检查上游验证——链中更早的输入解析、类型转换或守卫子句可能使"Bug"不可达。
  3. 阅读周围代码——中间件、拦截器或 init 函数可能设置了函数所依赖的状态。

当上下文降低了严重性但未消除问题时: 仍然以降低的优先级报告,并附上说明哪些上游保证保护了它。添加一个简短的内联注释(例如 // note: safe because caller validates via parseID() which returns uint),以便为未来的审查者记录推理过程。

7. 从简单开始

有时 fmt.Println 就是本地调试的正确工具。仅在更简单的方法失败时才升级工具。永远不要在生成环境调试中使用 fmt.Println——使用 slog

红旗:你调试错了

如果发生以下任何情况,停止并返回第 1 步:

  • "先快速修复,稍后调查"——没有"稍后"。找到根本原因。
  • 多个同时更改——一次一个假设。
  • 在未理解原因的情况下提出修复——"也许我在这里加个 nil 检查……"是猜测,不是调试。
  • 每次修复都揭示一个新问题——你在治疗症状。真正的 Bug 在别处。
  • 对同一问题尝试了 3 次以上修复——你的心智模型错了。重新阅读代码,从头追踪数据流。
  • "在我机器上能运行"——你还没有隔离环境差异。
  • 责怪框架/标准库/编译器——几乎不可能是 Go 的 Bug。先验证你的代码。

参考文件

  • 通用调试方法论 — 系统化的 10 步流程:定义症状、隔离复现、形成一个假设、测试它、验证根本原因、防止回归。升级指南:何时从 fmt.Println 升级到日志再到 pprof 再到 Delve,以及如何避免同时进行多个更改的陷阱。

  • 常见 Go Bug — 导致 Go 代码崩溃的 Bug:nil 指针解引用、接口 nil 陷阱(类型化 nil ≠ nil)、变量遮蔽、切片/映射/defer/错误/上下文陷阱、竞态条件、JSON 反序列化意外、未关闭资源。每个都附有复现模式和修复方法。

  • 测试驱动调试 — 为什么编写一个失败的测试是调试的第一步。涵盖测试隔离技术、用于缩小失败范围的表驱动测试组织、有用的 go test 标志(-v-run-count=10 用于不稳定测试),以及调试不稳定测试。

  • 并发调试 — 竞态条件、死锁、goroutine 泄漏。何时使用竞态检测器(-race)、如何读取竞态检测器输出、隐藏竞态的模式、使用 goleak 检测泄漏、分析堆栈转储以寻找死锁线索。

  • 性能故障排查 — 当代码慢时:CPU 性能分析工作流、内存分析(heap vs alloc_objects 性能分析、查找泄漏)、锁争用(mutex 性能分析)和 I/O 阻塞(goroutine 性能分析)。如何阅读火焰图、识别热点函数、并通过基准测试衡量改进。

  • pprof 参考 — 完整的 pprof 手册。如何在生产环境中启用 pprof 端点(带认证)、性能分析类型(CPU、heap、goroutine、mutex、block、trace)、本地和远程捕获性能分析、交互式分析命令(toplistweb)以及解释火焰图。

  • 诊断工具 — 针对特定症状的辅助工具。GODEBUG 环境变量(GC 追踪、调度器追踪)、用于断点调试的 Delve 调试器、逃逸分析(go build -gcflags="-m" 以查找意外的堆分配)、Go 执行追踪器以理解 goroutine 调度。

  • 生产环境调试 — 在不停止服务的情况下调试生产系统。生产检查清单、结构化日志以便搜索、安全启用 pprof(认证、网络隔离)、从运行服务捕获性能分析、网络调试(tcpdump、netstat)以及 HTTP 请求/响应检查。

  • 编译问题 — 构建失败:模块版本冲突、CGO 链接问题、go.mod 与安装的 Go 版本不匹配、平台特定的构建标签阻止交叉编译。

  • 代码审查红旗 — 代码审查中需要注意的模式,这些模式暗示潜在 Bug:未检查的错误、缺少 nil 检查、并发映射访问、没有明确退出的 goroutine、循环中 defer 导致的资源泄漏。

交叉引用

  • → 参见 samber/cc-skills-golang@golang-performance 技能,了解识别瓶颈后的优化模式
  • → 参见 samber/cc-skills-golang@golang-observability 技能,了解 Go 运行时监控的指标、告警和 Grafana 仪表板
  • → 参见 samber/cc-skills@promql-cli 技能,了解在生产事件调查期间查询 Prometheus 指标
  • → 参见 samber/cc-skills-golang@golang-concurrencysamber/cc-skills-golang@golang-safetysamber/cc-skills-golang@golang-error-handling 技能