停止使用 Println 调试 Go,改用 Delve

Delve 包含丰富的功能,使调试变得轻而易举。
161 位读者喜欢这篇文章。
Bug tracking magnifying glass on computer screen

Pixabay, testbytes, CC0

你上次尝试学习一门新的编程语言是什么时候?你是坚持使用你久经考验的语言,还是属于那种一旦有新语言发布就勇敢尝试的人?无论哪种方式,学习一门新的语言都非常有用,而且充满乐趣。

你从一个简单的 "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-release 
Fedora release 30 (Thirty)
$ 
$ go version
go version go1.12.17 linux/amd64
$ 

Golang 安装

如果你没有安装 Go,你可以通过简单地运行以下命令从你配置的存储库中获取它。

$ dnf install golang.x86_64

或者,你可以访问 安装页面,了解适合你的操作系统发行版的其他安装选项。

在我们开始之前,请确保已设置以下 Go 工具所需的必要 PATH。如果未设置这些路径,某些示例可能无法正常工作。这些可以很容易地设置为你 SHELL 的 RC 文件中的环境变量,例如我的情况下的 $HOME/bashrc 文件。

$ go env | grep GOPATH
GOPATH="/home/user/go"
$ 
$ go env | grep GOBIN
GOBIN="/home/user/go/gobin"
$ 

Delve 安装

你可以通过运行一个简单的 go get 命令来安装 Delve,如下所示。go get 是 Golang 从外部源下载和安装所需软件包的方式。如果你在安装过程中遇到任何问题,请参阅此处的 Delve 安装说明

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

运行上述命令会将 Delve 下载到你的 $GOPATH 位置,在默认情况下,该位置恰好是 $HOME/go。如果你已将 $GOPATH 设置为其他位置,则会有所不同。

你可以移动到 go/ 目录,在该目录下,你将在 bin/ 目录下看到 dlv

$ ls -l $HOME/go
total 8
drwxrwxr-x. 2 user user 4096 May 25 19:11 bin
drwxrwxr-x. 4 user user 4096 May 25 19:21 src
$ 
$ ls -l ~/go/bin/
total 19596
-rwxrwxr-x. 1 user user 20062654 May 25 19:17 dlv
$ 

由于你已在 $GOPATH 下安装了 Delve,因此它也可以作为常规 shell 命令使用,因此你不必每次都移动到安装它的目录。你可以通过使用 version 选项运行它来验证 dlv 是否已正确安装。它安装的版本是 1.4.1。

$ which dlv
~/go/bin/dlv
$ 
$ dlv version
Delve Debugger
Version: 1.4.1
Build: $Id: bda606147ff48b58bde39e20b9e11378eaa4db46 $
$ 

现在,让我们将 Delve 与一些 Go 程序一起使用,以了解其功能以及如何使用它们。与所有程序一样,让我们从一个简单的 "Hello, world!" 消息开始,在 Go 中,它被称为 hello.go

请记住,我将这些示例程序放在 $GOBIN 目录中。

$ pwd
/home/user/go/gobin
$ 
$ cat hello.go 
package main

import "fmt"

func main() {
	fmt.Println("Hello, world!")
}
$ 

要构建 Go 程序,你需要运行 build 命令,并使用 .go 扩展名向其提供源文件。如果程序没有任何语法问题,Go 编译器会编译它并输出一个二进制或可执行文件。然后可以直接执行此文件,我们会在屏幕上看到 "Hello, world!" 消息。

$ go build hello.go 
$ 
$ ls -l hello
-rwxrwxr-x. 1 user user 1997284 May 26 12:13 hello
$ 
$ ./hello 
Hello, world!
$ 

在 Delve 中加载程序

有两种方法可以将程序加载到 Delve 调试器中。



当源代码尚未编译为二进制文件时,使用 debug 参数。



第一种方法是在你只需要源文件时使用 debug 命令。Delve 会为你编译一个名为 __debug_bin 的二进制文件,并将其加载到调试器中。

在此示例中,移动到 hello.go 所在的目录并运行 dlv debug 命令。如果一个目录中有多个 Go 源文件,并且每个文件都有自己的 main 函数,那么 Delve 可能会抛出错误,期望从单个程序或单个项目构建二进制文件。如果发生这种情况,你最好使用下面介绍的第二种选项。

$ ls -l
total 4
-rw-rw-r--. 1 user user 74 Jun  4 11:48 hello.go
$
$ dlv debug
Type 'help' for list of commands.
(dlv)



现在打开另一个终端并列出同一目录的内容。你将看到一个额外的 __debug_bin 二进制文件,该文件是从源代码编译并加载到调试器中的。现在你可以移动到 dlv 提示符以继续进一步使用 Delve。

 

$ ls -l
total 2036
-rwxrwxr-x. 1 user user 2077085 Jun  4 11:48 __debug_bin
-rw-rw-r--. 1 user user      74 Jun  4 11:48 hello.go
$



使用 exec 参数

将程序加载到 Delve 的第二种方法在你有一个预编译的 Go 二进制文件,或者你已经使用 go build 命令编译了一个二进制文件,并且不想让 Delve 将其编译为 __debug_bin 二进制文件时很有用。在这种情况下,使用 exec 参数将二进制文件目录加载到 Delve 调试器中。

$ ls -l
total 4
-rw-rw-r--. 1 user user 74 Jun  4 11:48 hello.go
$
$ go build hello.go
$
$ ls -l
total 1956
-rwxrwxr-x. 1 user user 1997284 Jun  4 11:54 hello
-rw-rw-r--. 1 user user      74 Jun  4 11:48 hello.go
$
$ dlv exec ./hello
Type 'help' for list of commands.
(dlv)

在 Delve 中获取帮助

dlv 提示符下,你可以运行 help 来查看 Delve 中可用的各种帮助选项。命令列表非常广泛,我们将在此处介绍一些重要功能。以下是 Delve 功能的概述。

(dlv) help
The following commands are available:

Running the program:

Manipulating breakpoints:

Viewing program variables and memory:

Listing and switching between threads and goroutines:

Viewing the call stack and selecting frames:

Other commands:

Type help followed by a command for full documentation.
(dlv)

设置断点

现在我们已经在 Delve 调试器中加载了 hello.go 程序,让我们在 main 函数上设置断点,然后确认它。在 Go 中,主程序以 main.main 开头,因此你需要将此名称提供给 break command。接下来,我们将使用 breakpoints 命令查看断点是否已正确设置。

另外,请记住你可以使用命令的简写形式,因此你可以使用 b main.main 而不是 break main.main 来达到相同的效果,或者使用 bp 而不是 breakpoints。要查找命令的确切简写形式,请通过运行 help 命令参考帮助部分。

(dlv) break main.main
Breakpoint 1 set at 0x4a228f for main.main() ./hello.go:5
(dlv) breakpoints
Breakpoint runtime-fatal-throw at 0x42c410 for runtime.fatalthrow() /usr/lib/golang/src/runtime/panic.go:663 (0)
Breakpoint unrecovered-panic at 0x42c480 for runtime.fatalpanic() /usr/lib/golang/src/runtime/panic.go:690 (0)
	print runtime.curg._panic.arg
Breakpoint 1 at 0x4a228f for main.main() ./hello.go:5 (0)
(dlv)

继续执行程序

现在,让我们使用 "continue" 继续运行程序。它将运行直到它遇到断点,在我们的例子中,断点是 main.main 或 main 函数。从那里,我们可以使用 next 命令逐行执行程序。请注意,一旦我们超过 fmt.Println("Hello, world!"),我们可以看到 Hello, world! 被打印到屏幕上,而我们仍然在调试器会话中。

(dlv) continue
> main.main() ./hello.go:5 (hits goroutine(1):1 total:1) (PC: 0x4a228f)
     1:	package main
     2:	
     3:	import "fmt"
     4:	
=>   5:	func main() {
     6:		fmt.Println("Hello, world!")
     7:	}
(dlv) next
> main.main() ./hello.go:6 (PC: 0x4a229d)
     1:	package main
     2:	
     3:	import "fmt"
     4:	
     5:	func main() {
=>   6:		fmt.Println("Hello, world!")
     7:	}
(dlv) next
Hello, 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
$ 

让我们使用其他一些 Go 程序来探索 Delve 的其他一些功能。这次,我们将从 Golang tour 中选择一个程序。如果你正在学习 Go,Golang tour 应该是你的第一站。

以下程序 functions.go 只是展示了如何在 Go 程序中定义和调用函数。在这里,我们有一个简单的 add() 函数,它将两个数字相加并返回它们的值。你可以构建并执行该程序,如下所示。

$ cat functions.go 
package main

import "fmt"

func add(x int, y int) int {
	return x + y
}

func main() {
	fmt.Println(add(42, 13))
}
$

你可以构建并执行该程序,如下所示。

$ go build functions.go  && ./functions 
55
$ 

步入函数

如前所示,让我们使用前面提到的选项之一将二进制文件加载到 Delve 调试器中,再次在 main.main 处设置断点,并在我们命中断点时继续运行程序。然后点击 next 直到你到达 fmt.Println(add(42, 13)); 在这里我们调用 add() 函数。我们可以使用 Delve step 命令从 main 函数移动到 add() 函数,如下所示。

$ dlv debug
Type 'help' for list of commands.
(dlv) break main.main
Breakpoint 1 set at 0x4a22b3 for main.main() ./functions.go:9
(dlv) c
> main.main() ./functions.go:9 (hits goroutine(1):1 total:1) (PC: 0x4a22b3)
     4:	
     5:	func add(x int, y int) int {
     6:		return x + y
     7:	}
     8:	
=>   9:	func main() {
    10:		fmt.Println(add(42, 13))
    11:	}
(dlv) next
> main.main() ./functions.go:10 (PC: 0x4a22c1)
     5:	func add(x int, y int) int {
     6:		return x + y
     7:	}
     8:	
     9:	func main() {
=>  10:		fmt.Println(add(42, 13))
    11:	}
(dlv) step
> main.add() ./functions.go:5 (PC: 0x4a2280)
     1:	package main
     2:	
     3:	import "fmt"
     4:	
=>   5:	func add(x int, y int) int {
     6:		return x + y
     7:	}
     8:	
     9:	func main() {
    10:		fmt.Println(add(42, 13))
(dlv)

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

上面,我们经历了 main,然后移动到 add() 函数,但是你也可以使用 filename:linenumber 组合直接在你想要的位置设置断点。以下是在 add() 函数的开头设置断点的另一种方法。

(dlv) break functions.go:5
Breakpoint 1 set at 0x4a2280 for main.add() ./functions.go:5
(dlv) continue
> main.add() ./functions.go:5 (hits goroutine(1):1 total:1) (PC: 0x4a2280)
     1:	package main
     2:	
     3:	import "fmt"
     4:	
=>   5:	func add(x int, y int) int {
     6:		return x + y
     7:	}
     8:	
     9:	func main() {
    10:		fmt.Println(add(42, 13))
(dlv)

查看当前堆栈详细信息

现在我们位于 add() 函数中,我们可以使用 Delve 中的 stack 命令查看堆栈的当前内容。这显示了我们当前所在的顶层函数 add(),索引为 0,其次是从中调用 add() 函数的 main.main,索引为 1。main.main 下面的函数属于 Go 运行时,它负责加载和执行程序。

(dlv) stack
0  0x00000000004a2280 in main.add
   at ./functions.go:5
1  0x00000000004a22d7 in main.main
   at ./functions.go:10
2  0x000000000042dd1f in runtime.main
   at /usr/lib/golang/src/runtime/proc.go:200
3  0x0000000000458171 in runtime.goexit
   at /usr/lib/golang/src/runtime/asm_amd64.s:1337
(dlv) 

在帧之间移动

使用 Delve 中的 frame 命令,我们可以随意在上述帧之间切换。在下面的示例中,使用 frame 1 将我们从 add() 帧内切换到 main.main 帧,依此类推。

(dlv) frame 0
> main.add() ./functions.go:5 (hits goroutine(1):1 total:1) (PC: 0x4a2280)
Frame 0: ./functions.go:5 (PC: 4a2280)
     1:	package main
     2:	
     3:	import "fmt"
     4:	
=>   5:	func add(x int, y int) int {
     6:		return x + y
     7:	}
     8:	
     9:	func main() {
    10:		fmt.Println(add(42, 13))
(dlv) frame 1
> main.add() ./functions.go:5 (hits goroutine(1):1 total:1) (PC: 0x4a2280)
Frame 1: ./functions.go:10 (PC: 4a22d7)
     5:	func add(x int, y int) int {
     6:		return x + y
     7:	}
     8:	
     9:	func main() {
=>  10:		fmt.Println(add(42, 13))
    11:	}
(dlv)

打印函数参数

函数通常接受多个参数来处理。在 add() 函数的情况下,它接受两个整数。Delve 有一个方便的命令叫做 args,它可以显示传递给函数的命令行参数。

(dlv) args
x = 42
y = 13
~r2 = 824633786832
(dlv) 

查看反汇编

由于我们正在处理编译后的二进制文件,因此能够查看编译器生成的汇编语言指令非常有用。Delve 提供了 disassemble 命令来查看这些指令。在下面的示例中,我们使用它来查看 add() 函数的反汇编指令。

(dlv) step
> main.add() ./functions.go:5 (PC: 0x4a2280)
     1:	package main
     2:	
     3:	import "fmt"
     4:	
=>   5:	func add(x int, y int) int {
     6:		return x + y
     7:	}
     8:	
     9:	func main() {
    10:		fmt.Println(add(42, 13))
(dlv) disassemble
TEXT main.add(SB) /home/user/go/gobin/functions.go
=>	functions.go:5  0x4a2280   48c744241800000000   mov qword ptr [rsp+0x18], 0x0
	functions.go:6  0x4a2289   488b442408           mov rax, qword ptr [rsp+0x8]
	functions.go:6  0x4a228e   4803442410           add rax, qword ptr [rsp+0x10]
	functions.go:6  0x4a2293   4889442418           mov qword ptr [rsp+0x18], rax
	functions.go:6  0x4a2298   c3                   ret
(dlv)

跳出函数

另一个功能是 stepout,它允许我们返回到调用该函数的位置。在我们的示例中,如果我们希望返回到 main.main 函数,我们可以简单地运行 stepout 命令,它会将我们带回去。这可能是一个非常方便的工具,可以帮助你在大型代码库中移动。

(dlv) stepout
> main.main() ./functions.go:10 (PC: 0x4a22d7)
Values returned:
	~r2: 55

     5:	func add(x int, y int) int {
     6:		return x + y
     7:	}
     8:	
     9:	func main() {
=>  10:		fmt.Println(add(42, 13))
    11:	}
(dlv)

让我们使用 Go tour 中的另一个示例程序,看看 Delve 如何处理 Go 中的变量。以下示例程序定义并初始化了一些不同类型的变量。你可以构建并执行该程序。

$ cat variables.go 
package main

import "fmt"

var i, j int = 1, 2

func main() {
	var c, python, java = true, false, "no!"
	fmt.Println(i, j, c, python, java)
}
$ 

$ go build variables.go && ./variables
1 2 true false no!
$ 

打印变量信息

如前所述,使用 delve debug 将程序加载到调试器中。你可以从 Delve 中使用 print 命令以及变量名来显示它们的当前值。

(dlv) print c
true
(dlv) print java
"no!"
(dlv) 

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

(dlv) locals
python = false
c = true
java = "no!"
(dlv)

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

(dlv) whatis python
bool
(dlv) whatis c
bool
(dlv) whatis java
string
(dlv) 

结论

到目前为止,我们只触及了 Delve 提供的功能的表面。你可以参考 help 部分并尝试各种其他命令。其他一些有用的功能包括将 Delve 附加到正在运行的 Go 程序(守护进程!)甚至使用 Delve 来探索 Golang 库的一些内部结构,前提是你已安装 Go 源代码包。继续探索吧!

接下来阅读
User profile image.
经验丰富的软件工程专业人士。主要兴趣是安全、Linux、恶意软件。喜欢在命令行工作。对底层软件和理解事物的工作原理感兴趣。此处表达的观点仅代表我个人,不代表我的雇主。

评论已关闭。

Creative Commons License本作品根据 Creative Commons Attribution-Share Alike 4.0 International License 许可。
© . All rights reserved.