当前位置 : 首页 - IT技术 - 正文

使用Delve代替Println来调试Go程序


发布时间:2020年07月10日

使用Delve代替Println来调试Go程序

Delve 是能让调试变成轻而易举的事的万能工具包。

你上次尝试去学习一种新的编程语言时什么时候?你有没有持之以恒,你是那些在新事物发布的第一时间就勇敢地去尝试的一员吗?不管怎样,学习一种新的语言也许非常有用,也会有很多乐趣。

你尝试着写简单的 “Hello, world!”,然后写一些示例代码并执行,继续做一些小的修改,之后继续前进。我敢保证我们都有过这个经历,不论我们使用哪种技术。假如你尝试用一段时间一种语言,并且你希望能够精通它,那么有一些事物能在你的进取之路上帮助你。

其中之一就是调试器。有些人喜欢在代码中用简单的 “print” 语句进行调试,这种方式很适合代码量少的简单程序;然而,如果你处理的是有多个开发者和几千行代码的大型项目,你应该使用调试器。

最近我开始学习 Go 编程语言了,在本文中,我们将探讨一种名为 Delve 的调试器。Delve 是专门用来调试 Go 程序的工具,我们会借助一些 Go 示例代码来了解下它的一些功能。不要担心这里展示的 Go 示例代码;即使你之前没有写过 Go 代码也能看懂。Go 的目标之一是简单,因此代码是始终如一的,理解和解释起来都很容易。

Delve 介绍

Delve 是托管在GitHub上的一个开源项目。

它自己的文档中写道:

Delve 是 Go 编程语言的调试器。该项目的目标是为 Go 提供一个简单、全功能的调试工具。Delve 应该是易于调用和易于使用的。当你使用调试器时,事情可能不会按你的思路运行。如果你这样想,那么你不适合用 Delve。

让我们来近距离看一下。

我的测试系统是运行着 Fedora Linux 的笔记本电脑,Go 编译器版本如下:

$cat/etc/fedora-releaseFedorarelease30(Thirty)$$ go versiongo version go1.12.17linux/amd64$

Golang 安装

如果你没有安装 Go,你可以运行下面的命令,很轻松地就可以从配置的仓库中获取。

$ dnf install golang.x86_64

或者,你可以在安装页面找到适合你的操作系统的其他安装版本。

在开始之前,请先确认已经设置好了 Go 工具依赖的下列各个路径。如果这些路径没有设置,有些示例可能不能正常运行。你可以在 SHELL 的 RC 文件中轻松设置这些环境变量,我的机器上是在$HOME/bashrc文件中设置的。

$ goenv|grepGOPATHGOPATH="/home/user/go"$$ goenv|grepGOBINGOBIN="/home/user/go/gobin"$

Delve 安装

你可以像下面那样,通过运行一个简单的go get命令来安装 Delve。go get是 Golang 从外部源下载和安装需要的包的方式。如果你安装过程中遇到了问题,可以查看Delve 安装教程

$ goget-u github.com/go-delve/delve/cmd/dlv$

运行上面的命令,就会把 Delve 下载到你的$GOPATH的位置,如果你没有把$GOPATH设置成其他值,那么默认情况下$GOPATH和$HOME/go是同一个路径。

你可以进入go/目录,你可以在bin/目录下看到dlv。

$ls-l $HOME/gototal8drwxrwxr-x.2user user4096May2519:11bindrwxrwxr-x.4user user4096May2519:21src$$ls-l~/go/bin/total19596-rwxrwxr-x.1user user20062654May2519:17dlv$

因为你把 Delve 安装到了$GOPATH,所以你可以像运行普通的 shell 命令一样运行它,即每次运行时你不必先进入它所在的目录。你可以通过version选项来验证dlv是否正确安装。示例中安装的版本是 1.4.1。

$ which dlv~/go/bin/dlv$$ dlv versionDelveDebuggerVersion:1.4.1Build:$Id:bda606147ff48b58bde39e20b9e11378eaa4db46 $$

现在,我们一起在 Go 程序中使用 Delve 来理解下它的功能以及如何使用它们。我们先来写一个hello.go,简单地打印一条Hello, world!信息。

记着,我把这些示例程序放到了$GOBIN目录下。

$pwd/home/user/go/gobin$$cathello.gopackagemainimport"fmt"func main(){fmt.Println("Hello, world!")}$

运行build命令来编译一个 Go 程序,它的输入是.go后缀的文件。如果程序没有语法错误,Go 编译器把它编译成一个二进制可执行文件。这个文件可以被直接运行,运行后我们会在屏幕上看到Hello, world!信息。

$ go build hello.go$$ls-l hello-rwxrwxr-x.1user user1997284May2612:13hello$$./helloHello,world!$

在 Delve 中加载程序

把一个程序加载进 Delve 调试器有两种方式。

在源码编译成二进制文件之前使用 debug 参数

第一种方式是在需要时对源码使用debug命令。Delve 会为你编译出一个名为__debug_bin的二进制文件,并把它加载进调试器。

在这个例子中,你可以进入hello.go所在的目录,然后运行dlv debug命令。如果目录中有多个源文件且每个文件都有自己的主函数,Delve 则可能抛出错误,它期望的是单个程序或从单个项目构建成单个二进制文件。如果出现了这种错误,那么你就应该用下面展示的第二种方式。

$ls-ltotal4-rw-rw-r--.1user user74Jun411:48hello.go$$ dlv debugType'help'forlistof commands.(dlv)

现在打开另一个终端,列出目录下的文件。你可以看到一个多出来的__debug_bin二进制文件,这个文件是由源码编译生成的,并会加载进调试器。你现在可以回到dlv提示框继续使用 Delve。

$ls-ltotal2036-rwxrwxr-x.1user user2077085Jun411:48__debug_bin-rw-rw-r--.1user user74Jun411:48hello.go$

使用 exec 参数

如果你已经有提前编译好的 Go 程序或者已经用go build命令编译完成了,不想再用 Delve 编译出__debug_bin二进制文件,那么第二种把程序加载进 Delve 的方法在这些情况下会很有用。在上述情况下,你可以使用exec命令来把整个目录加载进 Delve 调试器。

$ls-ltotal4-rw-rw-r--.1user user74Jun411:48hello.go$$ go build hello.go$$ls-ltotal1956-rwxrwxr-x.1user user1997284Jun411:54hello-rw-rw-r--.1user user74Jun411:48hello.go$$ dlvexec./helloType'help'forlistof commands.(dlv)

查看 delve 帮助信息

在dlv提示符中,你可以运行help来查看 Delve 提供的多种帮助选项。命令列表相当长,这里我们只列举一些重要的功能。下面是 Delve 的功能概览。

(dlv)helpThefollowing commands are available:Runningthe program:Manipulatingbreakpoints:Viewingprogram variablesandmemory:Listingandswitching between threadsandgoroutines:Viewingthe call stackandselecting frames:Othercommands:Typehelp followed by a commandforfull documentation.(dlv)

设置断点

现在我们已经把 hello.go 程序加载进了 Delve 调试器,我们在主函数处设置断点,稍后来确认它。在 Go 中,主程序从main.main处开始执行,因此你需要给这个名字提供个break命令。之后,我们可以用breakpoints命令来检查断点是否正确设置了。

不要忘了你还可以用命令简写,因此你可以用b main.main来代替break main.main,两者效果相同,bp和breakpoints同理。你可以通过运行help命令查看帮助信息来找到你想要的命令简写。

(dlv)breakmain.mainBreakpoint1setat0x4a228fformain.main()./hello.go:5(dlv)breakpointsBreakpointruntime-fatal-throwat0x42c410forruntime.fatalthrow()/usr/lib/golang/src/runtime/panic.go:663(0)Breakpointunrecovered-panic at0x42c480forruntime.fatalpanic()/usr/lib/golang/src/runtime/panic.go:690(0)printruntime.curg._panic.argBreakpoint1at0x4a228fformain.main()./hello.go:5(0)(dlv)

程序继续执行

现在,我们用continue来继续运行程序。它会运行到断点处中止,在我们的例子中,会运行到主函数的main.main处中止。从这里开始,我们可以用next命令来逐行执行程序。请注意,当我们运行到fmt.Println("Hello, world!")处时,即使我们还在调试器里,我们也能看到打印到屏幕的Hello, world!。

(dlv)continue>main.main()./hello.go:5(hits goroutine(1):1total:1)(PC:0x4a228f)1:packagemain2:3:import"fmt"4:=>5:func main(){6:fmt.Println("Hello, world!")7:}(dlv)next>main.main()./hello.go:6(PC:0x4a229d)1:packagemain2:3:import"fmt"4:5:func main(){=>6:fmt.Println("Hello, world!")7:}(dlv)nextHello,world!>main.main()./hello.go:7(PC:0x4a22ff)2:3:import"fmt"4:5:func main(){6:fmt.Println("Hello, world!")=>7:}(dlv)

退出 Delve

你随时可以运行quit命令来退出调试器,退出之后你会回到 shell 提示符。相当简单,对吗?

(dlv)quit$

Delve 的其他功能

我们用其他的 Go 程序来探索下 Delve 的其他功能。这次,我们从golang 教程中找了一个程序。如果你要学习 Go 语言,那么 Golang 教程应该是你的第一站。

下面的程序,functions.go中简单展示了 Go 程序中是怎样定义和调用函数的。这里,我们有一个简单的把两数相加并返回和值的add()函数。你可以像下面那样构建程序并运行它。

$catfunctions.gopackagemainimport"fmt"func add(xint,yint)int{returnx+y}func main(){fmt.Println(add(42,13))}$

你可以像下面那样构建和运行程序。

$ go build functions.go&&./functions55$

进入函数

跟前面展示的一样,我们用前面提到的一个选项来把二进制文件加载进 Delve 调试器,再一次在main.main处设置断点,继续运行程序直到断点处。然后执行next直到fmt.Println(add(42, 13))处;这里我们调用了add()函数。我们可以像下面展示的那样,用 Delve 的step命令从main函数进入add()函数。

$ dlv debugType'help'forlistof commands.(dlv)breakmain.mainBreakpoint1setat0x4a22b3formain.main()./functions.go:9(dlv)c>main.main()./functions.go:9(hits goroutine(1):1total:1)(PC:0x4a22b3)4:5:func add(xint,yint)int{6:returnx+y7:}8:=>9:func main(){10:fmt.Println(add(42,13))11:}(dlv)next>main.main()./functions.go:10(PC:0x4a22c1)5:func add(xint,yint)int{6:returnx+y7:}8:9:func main(){=>10:fmt.Println(add(42,13))11:}(dlv)step>main.add()./functions.go:5(PC:0x4a2280)1:packagemain2:3:import"fmt"4:=>5:func add(xint,yint)int{6:returnx+y7:}8:9:func main(){10:fmt.Println(add(42,13))(dlv)

使用文件名:行号来设置断点

上面的例子中,我们经过main函数进入了add()函数,但是你也可以在你想加断点的地方直接使用“文件名:行号”的组合。下面是在add()函数开始处加断点的另一种方式。

(dlv)breakfunctions.go:5Breakpoint1setat0x4a2280formain.add()./functions.go:5(dlv)continue>main.add()./functions.go:5(hits goroutine(1):1total:1)(PC:0x4a2280)1:packagemain2:3:import"fmt"4:=>5:func add(xint,yint)int{6:returnx+y7:}8:9:func main(){10:fmt.Println(add(42,13))(dlv)

查看当前的栈信息

现在我们运行到了add()函数,我们可以在 Delve 中用stack命令查看当前栈的内容。这里在0位置展示了栈顶的函数add(),紧接着在1位置展示了调用add()函数的main.main。在main.main下面的函数属于 Go 运行时,是用来处理加载和执行该程序的。

(dlv)stack00x00000000004a2280inmain.addat./functions.go:510x00000000004a22d7inmain.mainat./functions.go:1020x000000000042dd1finruntime.mainat/usr/lib/golang/src/runtime/proc.go:20030x0000000000458171inruntime.goexitat/usr/lib/golang/src/runtime/asm_amd64.s:1337(dlv)

在帧之间跳转

在 Delve 中我们可以用frame命令实现帧之间的跳转。在下面的例子中,我们用frame实现了从add()帧跳到main.main帧,以此类推。

(dlv)frame0>main.add()./functions.go:5(hits goroutine(1):1total:1)(PC:0x4a2280)Frame0:./functions.go:5(PC:4a2280)1:packagemain2:3:import"fmt"4:=>5:func add(xint,yint)int{6:returnx+y7:}8:9:func main(){10:fmt.Println(add(42,13))(dlv)frame1>main.add()./functions.go:5(hits goroutine(1):1total:1)(PC:0x4a2280)Frame1:./functions.go:10(PC:4a22d7)5:func add(xint,yint)int{6:returnx+y7:}8:9:func main(){=>10:fmt.Println(add(42,13))11:}(dlv)

打印函数参数

一个函数通常会接收多个参数。在add()函数中,它的入参是两个整型。Delve 有个便捷的args命令,它能打印出命令行传给函数的参数。

(dlv)argsx=42y=13~r2=824633786832(dlv)

查看反汇编码

由于我们是调试编译出的二进制文件,因此如果我们能查看编译器生成的汇编语言指令将会非常有用。Delve 提供了一个disassemble命令来查看这些指令。在下面的例子中,我们用它来查看add()函数的汇编指令。

(dlv)step>main.add()./functions.go:5(PC:0x4a2280)1:packagemain2:3:import"fmt"4:=>5:func add(xint,yint)int{6:returnx+y7:}8:9:func main(){10:fmt.Println(add(42,13))(dlv)disassembleTEXT main.add(SB)/home/user/go/gobin/functions.go=>functions.go:50x4a228048c744241800000000mov qword ptr[rsp+0x18],0x0functions.go:60x4a2289488b442408mov rax,qword ptr[rsp+0x8]functions.go:60x4a228e4803442410add rax,qword ptr[rsp+0x10]functions.go:60x4a22934889442418mov qword ptr[rsp+0x18],raxfunctions.go:60x4a2298c3 ret(dlv)

单步退出函数

另一个功能是stepout,这个功能可以让我们跳回到函数被调用的地方。在我们的例子中,如果我们想回到main.main函数,我们只需要简单地运行stepout命令,它就会把我们带回去。在我们调试大型代码库时,这个功能会是一个非常便捷的工具。

(dlv)stepout>main.main()./functions.go:10(PC:0x4a22d7)Valuesreturned:~r2:555:func add(xint,yint)int{6:returnx+y7:}8:9:func main(){=>10:fmt.Println(add(42,13))11:}(dlv)

打印变量信息

我们一起通过Go 教程的另一个示例程序来看下 Delve 是怎么处理 Go 中的变量的。下面的示例程序定义和初始化了一些不同类型的变量。你可以构建和运行程序。

$catvariables.gopackagemainimport"fmt"vari,jint=1,2func main(){varc,python,java=true,false,"no!"fmt.Println(i,j,c,python,java)}$$ go build variables.go&&;./variables12truefalseno!$

像前面说过的那样,用delve debug在调试器中加载程序。你可以在 Delve 中用print命令通过变量名来展示他们当前的值。

(dlv)printctrue(dlv)printjava"no!"(dlv)

或者,你还可以用locals命令来打印函数内所有的局部变量。

(dlv)localspython=falsec=truejava="no!"(dlv)

如果你不知道变量的类型,你可以用whatis命令来通过变量名来打印它的类型。

(dlv)whatispythonbool(dlv)whatiscbool(dlv)whatisjavastring(dlv)

总结

现在我们只是了解了 Delve 所有功能的皮毛。你可以自己去查看帮助内容,尝试下其它的命令。你还可以把 Delve 绑定到运行中的 Go 程序上(守护进程!),如果你安装了 Go 源码库,你甚至可以用 Delve 导出 Golang 库内部的信息。勇敢去探索吧!