Go 调试入门:从 fmt.Println 到 go test、race 和 Delve

本文讲解 Go 初学者常用的调试方式,包括打印日志、最小复现、go test、race 检测、堆栈信息和 Delve 调试器。

调试不是乱试,而是缩小问题范围

写 Go 时,bug 可能来自很多地方:变量被遮蔽、切片共享底层数组、map 遍历顺序不稳定、JSON 字段没导出、goroutine 没退出、并发写 map、错误被忽略。初学阶段最重要的调试能力不是记住某个高级工具,而是学会缩小范围:问题在哪个函数发生?输入是什么?预期是什么?实际是什么?

Go 工具链提供了很多帮助。最简单的是打印,最可靠的是测试复现,并发问题可以用 race detector,复杂运行时问题可以用 Delve 单步调试。你不需要一开始全都精通,但要知道遇到不同问题时该用什么。

这篇文章整理一套入门调试流程:先复现,再缩小,再验证。

打印也要有方法

最简单:

fmt.Println(value)

打印结构体:

fmt.Printf("%+v\n", user)

%+v 会带字段名。打印类型:

fmt.Printf("%T\n", value)

打印带引号字符串:

fmt.Printf("%q\n", input)

%q 很适合检查字符串里是否有空格、换行或不可见字符。

调试 JSON:

data, _ := json.MarshalIndent(value, "", "  ")
fmt.Println(string(data))

打印不是坏事,但不要让临时打印长期留在业务代码里。服务里更应该使用日志,并带上上下文:请求 ID、用户 ID、文件路径、外部接口地址等。

最小复现比猜测更有用

假设你怀疑切片截取有问题,不要在几千行服务里来回猜。写一个小测试:

func TestSliceSharing(t *testing.T) {
	nums := []int{1, 2, 3, 4}
	part := nums[1:3]
	part[0] = 99

	t.Logf("nums=%v part=%v", nums, part)
}

运行:

go test -run TestSliceSharing -v

-run 可以只跑匹配名称的测试,-v 会显示 t.Logf 输出。最小复现能帮你确认语言行为,也能作为之后的回归测试。

很多 bug 修不好,是因为问题范围太大。先把它缩成一个小测试,思路会清楚很多。

使用 race detector 查并发问题

并发读写共享变量可能产生数据竞争。Go 提供 race detector:

go test -race ./...

也可以运行程序:

go run -race .

示例问题:

func TestRace(t *testing.T) {
	count := 0
	var wg sync.WaitGroup

	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			count++
		}()
	}

	wg.Wait()
	t.Log(count)
}

多个 goroutine 同时写 count,race detector 会报告。修复可以用锁:

var mu sync.Mutex
mu.Lock()
count++
mu.Unlock()

或者让每个 goroutine 发送结果,由一个 goroutine 汇总。

race 检测会让程序变慢,不是生产运行方式,但在测试和排查并发问题时非常有价值。

看 panic 堆栈

Go 程序 panic 时会打印堆栈:

panic: runtime error: invalid memory address or nil pointer dereference
goroutine 1 [running]:
main.displayName(...)
    /path/main.go:12
main.main()
    /path/main.go:20

先看 panic 类型,再看最上面属于你代码的文件和行号。很多初学者看到一大串堆栈就慌,其实关键通常在前几行。

nil 指针常见原因:

var user *User
fmt.Println(user.Name)

map 未初始化:

var scores map[string]int
scores["go"] = 100 // panic

应该:

scores := make(map[string]int)

panic 不是普通错误处理机制。业务可预期失败应该返回 error,不要用 panic 表达用户输入错误。

Delve 单步调试

Delve 是 Go 常用调试器。安装后可以:

dlv debug

常用命令:

break main.main
continue
next
step
print variableName
locals
goroutines

调试测试:

dlv test

Delve 适合排查复杂控制流、变量变化和并发状态。很多编辑器也集成了 Delve,可以打断点、单步执行、查看变量。

不过不要把调试器当成唯一工具。能用测试复现的问题,优先写测试。调试器帮你观察当下,测试帮你防止以后再坏。

小结

Go 调试可以从简单工具开始:fmt.Printf("%+v") 看结构体,%q 看字符串细节,go test -run 做最小复现,go test -race 查数据竞争,panic 堆栈看最上面的业务代码行,复杂问题用 Delve 单步调试。

真正有效的调试流程是:稳定复现,缩小范围,观察输入输出,提出假设,修改后用测试验证。不要在大段代码里盲目改来改去。

调试能力会直接影响开发速度。你越能快速把问题缩到一个函数、一条输入、一段堆栈,Go 的简单工具链就越能发挥作用。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页