Go Book / 2 Go Advances / 14 Go Debug

14 Go Debug

一、Debug概述

在程序开发过程中,多多少少会出现编译出错,运行时错误等等,有些是语法错误,有些是拼写错误,有些是语言特性没理解到位,这些因素都会造成程序开发的暂时中断,影响开发进度。所以常见的错误还是要认识并避免的,这里有一篇国外Gopher整理的《Go 陷阱及常见错误》,可以让你快速定位常见的问题,缩短debug的时间。但现实世界并非一帆风顺,有时让你走运遇到一些并不常见,或者不容易发现的错误时,你需要一些调试工具来帮你排查错误,以下就来介绍Go的调试工具。

二、Go 调试工具

目前Go语言支持GDB、LLDB和Delve几种调试器。其中GDB是最早支持的调试工具,LLDB是macOS系统推荐的标准调试工具。但是GDB和LLDB对Go语言的专有特性都缺乏很大支持,而只有Delve是专门为Go语言设计开发的调试工具。而且Delve本身也是采用Go语言开发,对Windows平台也提供了一样的支持。本节我们基于Delve简单解释如何调试Go汇编程序。

Delve

Delve是Go程序的源代码级调试器,它使你能够通过控制流程的执行来与你的程序进行交互,评估变量,并提供线程/goroutine状态,CPU寄存器状态等信息。此工具的目标是为调试Go程序提供简单而强大的界面。

使用--将标志传递给正在调试的程序,例如:

dlv exec ./hello -- server --config conf/config.toml

1.启动选项
  --accept-multiclient: 允许headless服务器接受多个客户端连接。请注意,服务器API不可重入,客户端必须进行协调。
  --api-version int :headless时选择API版本。(默认1)
  --backend string  :后端选择,可能的值为:
        	default :  在macOS上使用lldb,在其他地方使用native。
        	native :   原生的后端。
        	lldb:      使用lldb-server或debugserver。
        	rr :       使用mozilla rr(https://github.com/mozilla/rr)。
    (默认为“default”)
  --build-flags string:构建标志,传递给编译器。
  --check-go-version:检查使用的Go版本是否与Delve兼容。(默认为true)
  --headless:仅在headless模式下运行调试服务器。
  --init string :Init文件,由终端客户端执行。
  -l,--listen string :调试服务器监听地址。(默认为“localhost:0”)
  --log:启用调试服务器日志记录。
  --log-dest string :将日志写入指定的文件或文件描述符。如果参数是数字,则它将被解释为文件描述符,否则将被解释为文件路径。此选项还将在headless模式下重定向“API侦听”消息。
  --log-output string:以逗号分隔的组件列表,它们应该产生调试输出,可能的值:
        	debugger:      日志调试器命令
        	gdbwire:       与gdbserial后端的日志连接
        	lldbout:       将debugserver / lldb的输出复制到标准输出
        	debuglineerr:  记录可恢复的错误,读取.debug_line
        	rpc:           记录所有RPC消息
        	fncall:        日志函数调用协议
        	minidump:      记录minidump加载
            使用--log启用日志记录时,默认为“debugger”。
  --wd string:     运行程序的工作目录。(默认“.”)
也可以看看dlv下的各个工具包,帮助你进行不同要求的调试:
  • [dlv attach](dlv_attach.md) - 附加到正在运行的进程并开始调试。
  • [dlv connect](dlv_connect.md) - 连接到服务器调试。
  • [dlv core](dlv_core.md) - 检查核心转储。
  • [dlv debug](dlv_debug.md) - 编译并开始调试当前目录或指定包中的主包。
  • [dlv exec](dlv_exec.md) - 执行预编译二进制文件,并开始调试会话。
  • [dlv replay](dlv_replay.md) - 重播rr跟踪。
  • [dlv run](dlv_run.md) - 不推荐使用的命令。请改用“debug”。
  • [dlv test](dlv_test.md) - 编译测试二进制文件并开始调试程序。
  • [dlv trace](dlv_trace.md) - 编译并开始跟踪程序。
  • [dlv version](dlv_version.md) - 打印版本。

其内部命令和选项可查看文档,这里不再列出: Github delve 文档

简单入门

首先根据官方的文档正确安装Delve调试器。我们会先构造一个简单的Go语言代码,用于熟悉下Delve的简单用法。

创建main.go文件,main函数先通过循初始化一个切片,然后输出切片的内容:

package main

import (
	"fmt"
)

func main() {
	nums := make([]int, 5)
	for i := 0; i < len(nums); i++ {
		nums[i] = i * i
	}
	fmt.Println(nums)
}

下面我们用dlv debug工具简单使用一下,命令行进入包所在目录,然后输入dlv debug命令进入调试:

$ dlv debug
Type 'help' for list of commands.
(dlv)

输入help命令可以查看到Delve提供的调试命令列表:

(dlv) help
The following commands are available:
    args ------------------------ Print function arguments.
    break (alias: b) ------------ Sets a breakpoint.
    breakpoints (alias: bp) ----- Print out info for active breakpoints.
    clear ----------------------- Deletes breakpoint.
    clearall -------------------- Deletes multiple breakpoints.
    condition (alias: cond) ----- Set breakpoint condition.
    config ---------------------- Changes configuration parameters.
    continue (alias: c) --------- Run until breakpoint or program termination.
    disassemble (alias: disass) - Disassembler.
    down ------------------------ Move the current frame down.
    exit (alias: quit | q) ------ Exit the debugger.
    frame ----------------------- Set the current frame, or execute command...
    funcs ----------------------- Print list of functions.
    goroutine ------------------- Shows or changes current goroutine
    goroutines ------------------ List program goroutines.
    help (alias: h) ------------- Prints the help message.
    list (alias: ls | l) -------- Show source code.
    locals ---------------------- Print local variables.
    next (alias: n) ------------- Step over to next source line.
    on -------------------------- Executes a command when a breakpoint is hit.
    print (alias: p) ------------ Evaluate an expression.
    regs ------------------------ Print contents of CPU registers.
    restart (alias: r) ---------- Restart process.
    set ------------------------- Changes the value of a variable.
    source ---------------------- Executes a file containing a list of delve...
    sources --------------------- Print list of source files.
    stack (alias: bt) ----------- Print stack trace.
    step (alias: s) ------------- Single step through program.
    step-instruction (alias: si)  Single step a single cpu instruction.
    stepout --------------------- Step out of the current function.
    thread (alias: tr) ---------- Switch to the specified thread.
    threads --------------------- Print out info for every traced thread.
    trace (alias: t) ------------ Set tracepoint.
    types ----------------------- Print list of types
    up -------------------------- Move the current frame up.
    vars ------------------------ Print package variables.
    whatis ---------------------- Prints type of an expression.
Type help followed by a command for full documentation.
(dlv)

每个Go程序的入口是main.main函数,我们可以用break在此设置一个断点:

(dlv) break main.main
Breakpoint 1 set at 0x10ae9b8 for main.main() ./main.go:7

然后通过breakpoints查看已经设置的所有断点:

(dlv) breakpoints
Breakpoint unrecovered-panic at 0x102a380 for runtime.startpanic()
    /usr/local/go/src/runtime/panic.go:588 (0)
        print runtime.curg._panic.arg
Breakpoint 1 at 0x10ae9b8 for main.main() ./main.go:7 (0)

我们发现除了我们自己设置的main.main函数断点外,Delve内部已经为panic异常函数设置了一个断点。

通过vars命令可以查看全部包级的变量。因为最终的目标程序可能含有大量的全局变量,我们可以通过一个正则参数选择想查看的全局变量:

(dlv) vars main
main.initdone· = 2
runtime.main_init_done = chan bool 0/0
runtime.mainStarted = true
(dlv)

然后就可以通过continue命令让程序运行到下一个断点处:

(dlv) continue
> main.main() ./main.go:7 (hits goroutine(1):1 total:1) (PC: 0x10ae9b8)
     2:
     3: import (
     4:         "fmt"
     5: )
     6:
=>   7: func main() {
     8:         nums := make([]int, 5)
     9:         for i := 0; i < len(nums); i++ {
    10:                 nums[i] = i * i
    11:         }
    12:         fmt.Println(nums)
(dlv)

输入next命令单步执行进入main函数内部:

(dlv) next
> main.main() ./main.go:8 (PC: 0x10ae9cf)
     3: import (
     4:         "fmt"
     5: )
     6:
     7: func main() {
=>   8:         nums := make([]int, 5)
     9:         for i := 0; i < len(nums); i++ {
    10:                 nums[i] = i * i
    11:         }
    12:         fmt.Println(nums)
    13: }
(dlv)

进入函数之后可以通过args和locals命令查看函数的参数和局部变量:

(dlv) args
(no args)
(dlv) locals
nums = []int len: 842350763880, cap: 17491881, nil

因为main函数没有参数,因此args命令没有任何输出。而locals命令则输出了局部变量nums切片的值:此时切片还未完成初始化,切片的底层指针为nil,长度和容量都是一个随机数值。

再次输入next命令单步执行后就可以查看到nums切片初始化之后的结果了:

(dlv) next
> main.main() ./main.go:9 (PC: 0x10aea12)
     4:         "fmt"
     5: )
     6:
     7: func main() {
     8:         nums := make([]int, 5)
=>   9:         for i := 0; i < len(nums); i++ {
    10:                 nums[i] = i * i
    11:         }
    12:         fmt.Println(nums)
    13: }
(dlv) locals
nums = []int len: 5, cap: 5, [...]
i = 17601536
(dlv)

此时因为调试器已经到了for语句行,因此局部变量出现了还未初始化的循环迭代变量i。

下面我们通过组合使用break和condition命令,在循环内部设置一个条件断点,当循环变量i等于3时断点生效:

(dlv) break main.go:10
Breakpoint 2 set at 0x10aea33 for main.main() ./main.go:10
(dlv) condition 2 i==3
(dlv)

然后通过continue执行到刚设置的条件断点,并且输出局部变量:

(dlv) continue
> main.main() ./main.go:10 (hits goroutine(1):1 total:1) (PC: 0x10aea33)
     5: )
     6:
     7: func main() {
     8:         nums := make([]int, 5)
     9:         for i := 0; i < len(nums); i++ {
=>  10:                 nums[i] = i * i
    11:         }
    12:         fmt.Println(nums)
    13: }
(dlv) locals
nums = []int len: 5, cap: 5, [...]
i = 3
(dlv) print nums
[]int len: 5, cap: 5, [0,1,4,0,0]
(dlv)

我们发现当循环变量i等于3时,nums切片的前3个元素已经正确初始化。

我们还可以通过stack查看当前执行函数的栈帧信息:

(dlv) stack
0  0x00000000010aea33 in main.main
   at ./main.go:10
1  0x000000000102bd60 in runtime.main
   at /usr/local/go/src/runtime/proc.go:198
2  0x0000000001053bd1 in runtime.goexit
   at /usr/local/go/src/runtime/asm_amd64.s:2361
(dlv)

或者通过goroutine和goroutines命令查看当前Goroutine相关的信息:

(dlv) goroutine
Thread 101686 at ./main.go:10
Goroutine 1:
  Runtime: ./main.go:10 main.main (0x10aea33)
  User: ./main.go:10 main.main (0x10aea33)
  Go: /usr/local/go/src/runtime/asm_amd64.s:258 runtime.rt0_go (0x1051643)
  Start: /usr/local/go/src/runtime/proc.go:109 runtime.main (0x102bb90)
(dlv) goroutines
[4 goroutines]
* Goroutine 1 - User: ./main.go:10 main.main (0x10aea33) (thread 101686)
  Goroutine 2 - User: /usr/local/go/src/runtime/proc.go:292 \
                runtime.gopark (0x102c189)
  Goroutine 3 - User: /usr/local/go/src/runtime/proc.go:292 \
                runtime.gopark (0x102c189)
  Goroutine 4 - User: /usr/local/go/src/runtime/proc.go:292 \
                runtime.gopark (0x102c189)
(dlv)

最后完成调试工作后输入quit命令退出调试器。至此我们已经掌握了Delve调试器器的简单用法。

总之,遇到bug时别慌张,当掌握一些常见错误和一些的调试方法后,你也可以科学的定位问题,高效的排查错误,让开发效率显著提升。