关于 stuckgo
stuckgo 是面向 Go 开发者的 MCP 服务和命令行工具,可根据 goroutine 转储排查程序挂起。它会归类调用栈、排除常见空闲 goroutine、排列可能原因,并支持比较转储以调查泄漏,全程无需连接服务。
主要功能
- 01审查 Go goroutine 转储
- 02归类相同的调用栈
- 03排除常见的空闲 goroutine
- 04排列可能的阻塞原因
- 05比较两份转储以检查增长
- 06按严重程度输出 JSON 或使 CI 失败
适用场景
- 01根据转储排查挂起的 Go 服务
- 02比较转储以检查 goroutine 泄漏
- 03查找等待锁或数据库连接的 goroutine
MCP 相关
MCP 服务提供工具,可从文本或文件审查单份 goroutine 转储,也可比较同一程序的两份转储。它分析转储数据,不会连接服务。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
2收录的工具列表 · 更新于 2 周前
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| after | string | 否 | 较晚时获取的 goroutine 转储文本 |
| before | string | 否 | 较早时获取的 goroutine 转储文本 |
| after_path | string | 否 | 包含较晚时获取的转储的文件路径 |
| before_path | string | 否 | 包含较早时获取的转储的文件路径 |
详细介绍
stuckgo:这个 Go 程序为什么卡住了?
一个 MCP 服务和命令行工具,可读取 Go goroutine 转储,并告诉你程序在等待什么。它能找到 goroutine 泄漏、锁堆积、数据库连接池耗尽,以及没有超时的 HTTP 调用。它只读取转储文本,因此绝不会连接你的服务。
服务卡住时可能有数千个 goroutine,其中大多数都很正常:监听器、读取循环、空闲工作线程。stuckgo 会将相同的堆栈分组,把通常处于空闲状态的堆栈单独列出,并对其余堆栈进行排序。当有 40 个 goroutine 在等待互斥锁时,它还会指出可能持有锁的对象。
你可以向 Agent 提问
- “我们的 Go 服务卡住了。这是 goroutine 转储。”
- “我们有 goroutine 泄漏吗?这是相隔 10 分钟的两份转储。”
- “这个服务为什么没有可用的数据库连接了?”
- “是什么在持有这个互斥锁?”
安装
作为 Claude Code 插件:
claude plugin marketplace add GillMacedo/stuckgo
claude plugin install stuckgo@stuckgo-marketplace
作为 MCP 服务,供任意客户端使用:
{"mcpServers": {"stuckgo": {"command": "npx", "args": ["-y", "[email protected]", "serve"]}}}
作为命令行工具,需使用 Node 18 或更高版本:
npx stuckgo review dump.txt
或者使用 Go 1.27 或更高版本:go install github.com/GillMacedo/stuckgo@latest。
用法
从导入了 net/http/pprof 的服务获取转储。这不会停止程序:
curl 'http://localhost:6060/debug/pprof/goroutine?debug=2' > dump.txt
kill -QUIT <pid> 也会输出转储,但会结束进程。崩溃输出也可以使用,GOTRACEBACK=all 会显示所有 goroutine,而不只是发生故障的 goroutine。
以下是 examples/ 中一个虚构服务的真实转储,运行时所在目录是本仓库的克隆:
$ go run . review examples/dumps/mutex-late.txt
43 goroutines, 1 normally idle
Groups:
40 sync.Mutex.Lock, up to 2 min main.(*store).Get (app/main.go:109)
1 chan receive, up to 2 min main.mutex (app/main.go:130)
1 select, up to 2 min main.(*store).Refresh (app/main.go:118)
HIGH SG107 main.(*store).Get (app/main.go:109)
40 goroutines waiting for a mutex, waiting up to 2 minutes. Possible holder, not proven: main.(*store).Refresh (app/main.go:118), in select.
Try: Look at what the holder does while it holds the lock. Don't hold a lock across network calls, channel operations or slow work.
要检查是否存在泄漏,请比较相隔一段时间获取的两份转储。与只看一份转储相比,堆栈数量持续增长,且 goroutine ID 保持不变,是有力得多的证据:
$ go run . compare examples/dumps/leak-early.txt examples/dumps/leak-late.txt
Goroutines: 344 -> 637 (+293)
Growing:
+293 342 -> 635 chan send main.lookupPrice.func1 (app/main.go:79)
Same goroutine IDs in the same place in both dumps:
342 chan send, up to 2 min main.lookupPrice.func1 (app/main.go:79)
使用 Go 1.27 或更高版本时,Go 也可以自行检查:/debug/pprof/goroutineleak?debug=2。stuckgo 也能读取该输出。
添加 --json 可输出 JSON;添加 --fail-on high 可在 CI 中以 1 退出。使用 - 从标准输入读取。
它能发现什么
| 规则 | 发现内容 |
|---|---|
| SG101 | Go 自身的“所有 goroutine 都在休眠”死锁 |
| SG102 | panic 或致命错误,以及发生位置 |
| SG103 | Go 的泄漏分析报告为泄漏的 goroutine |
| SG104 | 对 nil 通道的发送或接收操作,这类操作永远不会返回 |
| SG105 | 等待空闲 database/sql 连接的 goroutine |
| SG106 | 等待时间过长的出站 HTTP 调用 |
| SG107 | 大量 goroutine 堵在互斥锁上,并列出可能持锁的 goroutine |
| SG108 | 尚未完成的 WaitGroup,以及它启动了什么 |
| SG109 | 卡在通道发送操作上的 goroutine,这是常见的泄漏情况 |
| SG110 | 大量等待通道接收或 select 的 goroutine |
| SG111 | 长时间等待网络 I/O |
| SG112 | 长时间运行的系统调用 |
| SG113 | 长时间等待 sync.Cond.Wait |
| SG114 | 大量 goroutine 在同一行休眠 |
| SG115 | 在多个调用栈中启动了大部分 goroutine 的单个函数 |
| SG116 | 大量正在运行或等待 CPU 的 goroutine |
| SG117 | 大量 goroutine 在 debug=1 转储中停在同一行,但转储不会说明原因 |
严重程度取决于涉及的 goroutine 数量及其等待时长。这些阈值是经验判断。
MCP 工具
| 工具 | 功能 |
|---|---|
go_goroutine_review | 从文本或文件中审查单份转储 |
go_goroutine_compare | 比较同一程序的两份转储 |
限制
- 发现结果只是线索。 转储只反映某一时刻的情况。请结合代码检查每项发现。
- 它无法证明是谁持有锁。 Go 的转储不会提供此信息。“可能持锁者”是指同一类型方法中的另一个 goroutine。
- 单份转储无法证明存在泄漏, 除非它来自 Go 自身的泄漏分析。空闲工作池看起来和泄漏一样,因此请比较两份转储。
debug=1显示的信息较少。 它只提供计数,没有等待时长或 goroutine ID,也会隐藏 goroutine 等待的大部分信息。条件允许时请使用debug=2。- 等待时长只是粗略估计。 Go 只会打印一分钟或更长时间的等待,而且从垃圾回收时开始计时,因此没有显示时长并不意味着等待时间短。
- 它无法查看 CPU 使用情况。 如果程序运行缓慢,但没有 goroutine 停滞,请获取 CPU 性能分析文件。
- 通常处于空闲状态的调用栈列表很短: Go 运行时、
net/http、database/sql、gRPC 和golang.org/x/net/http2。其他库的后台循环会以组的形式出现,但很少被列为发现结果。 - 转储可能包含文件路径和程序中的值。
stuckgo会在本机运行,但 MCP 客户端会将工具结果发送给其模型。
常见问题
如何在 Go 中查找 goroutine 泄漏? 间隔一段时间获取两份转储,然后运行 stuckgo compare。如果某个调用栈数量不断增加,而 goroutine ID 保持不变,就值得调查。
如何从正在运行的 Go 程序获取 goroutine 转储? 导入 net/http/pprof,然后获取 /debug/pprof/goroutine?debug=2。请确保该端点不对外公开。
goroutine 转储中的 [chan send, 5 minutes] 是什么意思? 该 goroutine 在向一个没有接收者的通道发送数据时,已经阻塞了约 5 分钟。SG109 会检测这种情况。
为什么我的 Go 服务没有可用的数据库连接? 等待在 database/sql.(*DB).conn 中的 goroutine 表明连接池已满。SG105 会将其标出。请检查是什么占用了连接,例如执行缓慢的查询或未关闭的 Rows。
这和 panicparse 或 goroutine-inspect 有什么不同? panicparse 和 goroutine-inspect 可帮助用户阅读和探索转储。stuckgo 会为 Agent 提供按优先级排序的发现结果,可以比较两份转储,并排除通常处于空闲状态的内容。
测试情况
examples/programs/中的实际程序: SG101 到 SG109 的所有规则,以及 SG111、SG114 和 SG117,都在examples/dumps/中那些确实卡住的程序转储上触发了。大多数规则也在 Go 1.22 上运行过。- 实际服务:
examples/dumps/real/中处于空闲状态的 Prometheus 3.7.3 和 etcd 3.6.5,触发的结果均不超过低级别。Prometheus 会产生两个低级别的WaitGroup发现,这在该服务中属于正常情况。 - 仅有编写的示例: SG110、SG112、SG113、SG115 和 SG116。
- MCP 工具调用通过 MCP Inspector 进行。
目前尚未在生产环境的事故中使用过,也未在全新的 Claude 会话中使用过。
贡献
欢迎大家贡献,包括使用 AI Agent 的人。请参阅 CONTRIBUTING.md。适合新手的议题列在这里。
Pull Request 的处理流程
- 创建 PR。 Fork 仓库,在分支上进行开发,并将改动控制在一项。标题应说明用户会看到什么变化,因为它会成为变更日志条目:
feat:会列在“新增”下,fix:会列在“修复”下。 - 自动检查。 代码改动需要附带测试,并且
make test必须通过。超过 30 行的改动需要关联一个尚未关闭的议题(Closes #<number>),且 PR 的改动行数不得超过 800 行。如果缺少任何内容,系统会发表评论,告知需要修正之处。 - 审查。 Claude 会自动审查每个 PR。代码 PR 还会经过安全审查,并在沙盒中测试。随后,审查者会合并 PR 或要求修改。涉及 CI、Agent 设置、npm 或 Python 依赖、供应商代码或构建文件的 PR 无法通过这种方式合并,系统会发表评论说明原因。安全审查未通过的内容也同样无法合并。
- 发布。 影响用户安装内容的合并改动会在大约两小时内发布为新版本,并附带 GitHub Release;如果项目有 npm 包,也会一并发布。文档和 CI 改动会等到下一次发布。
AI 使用说明
本项目主要由 Claude Code 生成,并由维护者审查。
关于我
我是 Giulliano Macedo,在 GoTech Solutions 开发开发者工具。欢迎关注或联系我:
- GitHub:GillMacedo
- LinkedIn:giulliano
- X:@giumacedo
如果 stuckgo 找到了你的程序卡住的原因,点个星标可以帮助其他人发现它。
由 Giulliano Macedo、GoTech Solutions 构建 · LinkedIn · X
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。