Go项目调试应优先使用dlv而非go run,正确安装、启动(dlv debug)、设置断点(关注goroutine层级)、精准打印变量(p/args/locals),生产环境则用trace+log+pprof组合分析。
Go 项目跑不起来、变量值对不上、协程卡死——不是代码写得烂,是调试没用对工具和姿势。
Go 自带的 go run 只负责执行,出错就抛堆栈,没法查中间状态。真正要定位逻辑问题,必须上 dlv(Delve)。它不是“可选插件”,而是 Go 生产级调试的事实标准。
go install github.com/go-delve/delve/cmd/dlv@latest
dlv exec ./main,优先用 dlv debug(自动编译 + 加载调试信息,避免因优化导致变量不可见)dlv 启动报 could not launch process: could not get pid,大概率是没关 SIP 或未授权开发者工具,xcode-select --install 和重启终端常能解决Go 扩展,并在 .vscode/launch.json 中使用 "type": "dlv",而非过时的 "type": "go"
Go 的并发模型让传统单线程调试思维失效。一个 fmt.Println 看似简单,但可能被 17 个 goroutine 同时触发——你停下的那个,未必是你想查的那个。
dlv 命令行时,先执行 goroutines 查所有活跃 goroutine,再用 goroutine bt 看它的完整调用栈http.HandleFunc("/api", handler),真正逻辑可能在 handler 内部嵌套的闭包或 defer 里,得进到具体行号下断break main.go:42 是静态断点;break runtime.gopark 这类运行时函数断点,适合排查 goroutine 卡死,但会频繁触发,慎用goroutine == 123 && user.ID > 0
新手习惯狂打 fmt.Printf,结果日志刷屏、并发输出错乱、还污染生产代码。dlv 内置的表达式求值才是干净解法。
p req.URL.Path 直接打印请求路径,支持链式访问、类型断言(p req.Context().Value("user").(*User))args 显示当前函数入参,locals 显示局部变量,比翻源码快得多;但注意:若变量被编译器优化掉(如未被后续使用),locals 里不会出现p myMap(可能只显示地址),改用 p *myMap 或 pp myMap(pretty print)net.Listener 类型变量默认只显示字段名,加 config 命令开启详细模式:config substitute-path /home/user/go /go 可修复路径映射问题生产环境禁用 dlv 是常态。这时候调试不是“暂停看变量”,而是“埋点抓证据”。Go 的内置工具链足够支撑闭环分析。
go tool trace 是唯一能看清 goroutine 生命周期、阻塞事件、GC 毛刺的工具。生成 trace 文件后用 go tool trace trace.out 打开交互界面,重点看 “Goroutine analysis” 面板log.Println("failed"),用 log.WithFields(如 zerolog)或至少带上 reqID 和时间戳,否则多请求混在一起等于没日志runtime/pprof 不只是看 CPU:用 pprof.Lookup("goroutine").WriteTo(..., 1) 抓当前所有 goroutine stack,比 kill -SIGQUIT 更可控;内存泄漏则用 pprof.WriteHeapProfile 配合 go tool pprof 分析 top allocsGODEBUG=gctrace=1 或 GORACE=1,它们会在 stderr 输出底层行为,有时比业务日志更早暴露问题调试 Go 项目最常被忽略的,是 runtime 行为本身:GC 触发时机、调度器抢占、netpoll wait 状态。这些不会出现在你的代码里,但会决定你的断点停在哪、变量值为什么突然变、goroutine 为什么迟迟不调度——盯住 runtime 包里的关键符号,比多加十个 fmt 有用得多。