停止使用 Println 并改用 Delve 来调试 Go 代码

Bug tracking magnifying glass on computer screen

Delve 拥有丰富的功能,使调试变得轻而易举

2020 年 6 月 11 日 Gaurav Kamathe (红帽,通讯员)
Image credits : 

Pixabay, testbytes, CC0

您上一次尝试学习一门新的编程语言是什么时候?您是坚持您那久经考验的真实生活呢,还是作为一个勇敢的人,一有新的语言发布就会尝试?无论哪种方式,学习一门新语言都是非常有用的,而且非常有趣。

您可以尝试一个简单的「世界,您好!」然后继续编写一些示例代码并执行它,在此过程中再进行较小的更改,然后从那里继续前进。无论我们使用哪种技术,我相信我们都有过这样的经历。不过如果您能在一种语言上坚持一段时间的话,并且希望精通该语言,这里有一些方法可以帮助您。

其中之一就是调试器。有些人喜欢在代码中使用简单的 「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 工具的要求设置了以下必需的 PATHS。如果未设置这些路径,则某些示例可能无法运行。可以很容易地将这些设置为 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
$

由于您将 Delve 安装在 $GOPATH 下,因此它也可以作为常规的 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 程序,您需要使用 .go 扩展名将其与源文件一起提供并运行 build 命令。如果程序没有任何语法问题,则 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 源文件,并且每个文件都有其自己的主要功能,则 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 程序,让我们在主函数上设置断点,然后进行确认。 在 Go 中,主程序以 main.main 开头,因此您需要将此名称提供给 break command。 接下来,我们将查看是否使用 breakpoints 命令正确设置了断点。

此外,请记住,您可以使用命令的简写形式,因此除了使用 break main.main 之外,还可以使用 b main.main 来达到相同的效果,或者使用 bp 代替断点。 要查找命令的确切缩写,请通过运行 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.mainmain 函数。 从那里,我们可以使用下一条命令逐行执行程序。 请注意,一旦我们移至 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
$

Let's use some other Go programs to explore some other Delve features. This time, we will pick a program from the [Golang tour](https://tour.golang.org/basics/4). If you are learning Go, the Golang tour should be your first stop.

The following program, `functions.go`, simply displays how functions are defined and called in a Go program. Here, we have a simple `add()` function that adds two numbers and returns their value. You can build the program and execute it, as shown below.

$ 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)

使用 filename:linenumber 设置断点

上面,我们介绍了先进入 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,接下来在位置 1 的是调用了 add() 函数的 main.mainmain.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 教程 中的另一个示例程序来了解 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 源代码包。 继续探索!

感谢阅读

go
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://opensource.com/article/20/6/debu...

译文地址:https://learnku.com/go/t/47071

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!