用Delve调试Go代码的教程

365 阅读14分钟

"天才是1%的天赋和99%的勤奋"。

"e = mc²"

- 阿尔伯特-爱因斯坦

在软件开发领域,这些名言可以稍作改变。

"软件开发是1%的编程和99%的调试。"

"错误=更多的代码^2"

- 某个高级开发人员,可能

调试是所有开发人员必须经历的事情,它并不关心你的专业知识。这是一个令人沮丧的过程。犯错是人之常情,错误绝对会悄悄进入你的程序。我不知道为什么,但是错误就像你的代码一样是你的卵子,所以你要负责处理它们。

我们都喜欢打印调试...

如果你是计算机科学专业的本科生,那么你可能有过一次糟糕的经历,那就是一个项目似乎没有按照你想要的方式运行。这也会适用于其他开发者。我的代码不工作,我不明白为什么。

我不知道为什么会这样,但是开发人员有一个普遍的倾向,那就是倾向于打印调试。在你假设错误所在的代码部分,你会打印输入和输出变量。

打印调试对于修复简单的错误往往更快。调试最重要的部分是检查你是否向一个函数提供了正确的输入,以及该函数是否吐出了想要的输出。打印语句可以轻松地处理这个问题。然而,有一些讨厌的bug往往在一个函数中表现出来,但却源自一个遥远的函数。还有一些情况,你可能想检查调用堆栈,跟踪一个变量的寿命,等等。

相信我,你会遇到这样的情况,你会觉得你在浪费大量的时间来输入打印语句。你的代码看起来会很难看,而且你会迷失在自己的代码中。那些时候,你会希望自己知道如何使用调试器。

如果你还不相信,可以试着这样想。知道如何使用调试器是件好事。我认为,因为你不知道如何使用调试器而使用打印调试是不好的做法。像任何技能一样,不知道不应该是你不使用/学习它的主要原因。这就像因为你不知道如何烹饪而叫外卖。或者不锻炼是因为你不知道如何使用健身器材。或者不学习是因为你不理解一个概念......你明白了吧。

使用Delve调试器

有一个流行的调试器叫gdb,可以调试Go代码。然而,Go文档并不推荐它,而是指出你可以使用一个更好的替代品。Delve

Delve就像一个工具箱,有很多工具可以帮助你压制那些讨厌的bug。当你可以使用诱饵站时,你就不必徒手抓蟑螂了。Delve为你提供了杀虫剂、诱饵站、火把等。Delve是专门用来捕捉Go虫的,它的使用相当简单。

首先,我们来安装它。如果你正在运行Go v1.16或更高版本(你可能应该这样做),你可以使用下面这行。

$ go install github.com/go-delve/delve/cmd/dlv@latest

确保你通过运行这个来安装它。

$ dlv version

Delve是用来调试主包和测试的。不幸的是,它在调试main以外的包方面相当有限,因为Delve需要程序的工作可执行文件,而这需要一个main包。像我一样,如果你正在编写一个没有主包的库,你需要为你的代码编写测试,然后再进行调试。

重温一下我最常使用的命令,并附上一段示例代码

这是我们在这个例子中要用到的代码。

package main
import (
    "fmt"
    "math"
)
func main() {
    n1 := []float64{0.1, 0.2, 0.3, 0.4, 0.5}
    n2 := []float64{math.NaN(), 0.2, 0.3, 0.4, 0.5}
    mean1 := calcMean(n1)
    mean2 := calcMean(n2)
    fmt.Println("mean1:", mean1)
    fmt.Println("mean2:", mean2)
}
func calcMean(nums []float64) float64 {
    mean := 0.0
    for _, num := range nums {
        mean += num
    }
    mean /= float64(len(nums))
    return mean
}

这段代码真的很简单!主函数定义了两个片断n1和n2,并调用calcMean来计算每个均值。这段代码看起来很好,而且编译器也没有抱怨。如果有任何语法错误,我们甚至无法编译我们的代码。然而,这段代码实际上有一个小问题。你能猜到它可能是什么吗?

好吧,让我们运行这段代码,看看会发生什么。

$ go run main.go
mean1: 0.3
mean2: NaN

有趣的是,mean1似乎没有问题,但mean2看起来有点奇怪。我们想要一个数字,而不是一个NaN值。为什么会出现这种情况呢?让我们用调试器来弄清楚到底发生了什么。

cd进入包括你的main.go文件的目录,然后运行这个。

$ dlv debug

如果你正在调试测试,你可以cd到存放你的*_test.go文件的目录,然后运行这个。

$ dlv test

据我所知,Delve没有GUI前端,所以你需要用命令行来使用Delve。这很好,因为Delve有一个友好的CLI,你可以真正理解。

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

你会看到,我们已经进入了Delve的界面。现在我们可以运行Delve特定的命令,这将有助于我们修复这个错误。

(dlv) break main.go:8
Breakpoint 1 set at 0x496732 for main.main() ./main.go:8

第一个命令是break。它在你的代码中创建一个断点。一个断点是你的代码中的一个点,你可以停止执行。在本例中,我在文件main.go的第8行设置了一个断点。在一个可疑的代码前设置断点是很有用的。通常,它是一个接受输入并返回输出的函数。要进入断点,我们可以使用以下命令。

(dlv) continue
> main.main() ./main.go:8 (hits goroutine(1):1 total:1) (PC: 0x496732)
     3: import (
     4:         "fmt"
     5:         "math"
     6: )
     7:
=>   8: func main() {
     9:         n1 := []float64{0.1, 0.2, 0.3, 0.4, 0.5}
    10:         n2 := []float64{math.NaN(), 0.2, 0.3, 0.4, 0.5}
    11:
    12:         mean1 := calcMean(n1)
    13:         mean2 := calcMean(n2)

continue将,继续遍历代码,直到到达下一个断点。如果没有设置断点,continue将直接贯穿整个程序,完成运行。这里,continue将把我们带到第8行。注意,断点不会真正运行指定的行。现在让我们逐行进行。

(dlv) next
> main.main() ./main.go:9 (PC: 0x496749)
     4:         "fmt"
     5:         "math"
     6: )
     7:
     8: func main() {
=>   9:         n1 := []float64{0.1, 0.2, 0.3, 0.4, 0.5}
    10:         n2 := []float64{math.NaN(), 0.2, 0.3, 0.4, 0.5}
    11:
    12:         mean1 := calcMean(n1)
    13:         mean2 := calcMean(n2)
    14:
(dlv) next
> main.main() ./main.go:10 (PC: 0x4967fa)
     5:         "math"
     6: )
     7:
     8: func main() {
     9:         n1 := []float64{0.1, 0.2, 0.3, 0.4, 0.5}
=>  10:         n2 := []float64{math.NaN(), 0.2, 0.3, 0.4, 0.5}
    11:
    12:         mean1 := calcMean(n1)
    13:         mean2 := calcMean(n2)
    14:
    15:         fmt.Println("mean1:", mean1)

我们使用next命令来进入下一行。在这里,我们穿越了第9行和第10行,在这里我们定义了n1和n2。让我们看看这些片断在我们的调试器中是如何表示的。

(dlv) print n1
[]float64 len: 5, cap: 5, [0.1,0.2,0.3,0.4,0.5]
(dlv) print n2
Command failed: could not find symbol value for n2

我们使用打印命令来查看一个变量的细节。然而,我们在那里看到了一些奇怪的东西。 print n1可以工作,但print n2却不行。这是因为当我们在某一行时,该行不会被执行,直到我们进入下一行。n1被定义在第9行,而n2被定义在第10行。我们目前在第10行,这意味着第9行已经被执行,但第10行还没有被执行。要解决这个问题,我们只需要转到下一行。

(dlv) next
> main.main() ./main.go:12 (PC: 0x4968a5)
     7:
     8: func main() {
     9:         n1 := []float64{0.1, 0.2, 0.3, 0.4, 0.5}
    10:         n2 := []float64{math.NaN(), 0.2, 0.3, 0.4, 0.5}
    11:
=>  12:         mean1 := calcMean(n1)
    13:         mean2 := calcMean(n2)
    14:
    15:         fmt.Println("mean1:", mean1)
    16:         fmt.Println("mean2:", mean2)
    17: }
(dlv) print n1
[]float64 len: 5, cap: 5, [0.1,0.2,0.3,0.4,0.5]
(dlv) print n2
[]float64 len: 5, cap: 5, [NaN,0.2,0.3,0.4,0.5]

你可以看到print n2现在是如何按照预期工作的。

我们此刻正处于第12行,这就是使用调试器可以区别于打印调试的地方。Delve有一个功能,你可以步入一个函数。顾名思义,步入函数可以让我们进入函数,一步一步地检查该函数。我来告诉你这是什么意思。

(dlv) step
> main.calcMean() ./main.go:19 (PC: 0x496ac0)
    14:
    15:         fmt.Println("mean1:", mean1)
    16:         fmt.Println("mean2:", mean2)
    17: }
    18:
=>  19: func calcMean(nums []float64) float64 {
    20:         mean := 0.0
    21:         for _, num := range nums {
    22:                 mean += num
    23:         }
    24:         mean /= float64(len(nums))

使用step步入一个函数。我们可以看到我们的范围是如何从主函数变为calcMean函数的。

(dlv) args
nums = []float64 len: 5, cap: 5, [...]
~r0 = 0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004074232364696

使用args将向我们展示函数的参数。nums是calcMean的输入。~r0表示函数的返回值。r代表返回,0代表第0个返回值。如果我们有多个返回值,我们会有类似r0, r1, r2....~代表一个未命名的值。我们知道calcMean将返回一个float64,但是我们没有为它指定一个名称。

我们也可以通过使用print命令来检查函数输入。

(dlv) print nums
[]float64 len: 5, cap: 5, [0.1,0.2,0.3,0.4,0.5]

所以我们知道,输入没有任何问题。让我们继续吧。我们想跟踪平均值变量的值。要做到这一点,请使用display命令。

(dlv) display -a mean
0: mean = error could not find symbol value for mean
(dlv) next
> main.calcMean() ./main.go:20 (PC: 0x496ae5)
    15:         fmt.Println("mean1:", mean1)
    16:         fmt.Println("mean2:", mean2)
    17: }
    18:
    19: func calcMean(nums []float64) float64 {
=>  20:         mean := 0.0
    21:         for _, num := range nums {
    22:                 mean += num
    23:         }
    24:         mean /= float64(len(nums))
    25:
0: mean = error could not find symbol value for mean
(dlv) next
> main.calcMean() ./main.go:21 (PC: 0x496aeb)
    16:         fmt.Println("mean2:", mean2)
    17: }
    18:
    19: func calcMean(nums []float64) float64 {
    20:         mean := 0.0
=>  21:         for _, num := range nums {
    22:                 mean += num
    23:         }
    24:         mean /= float64(len(nums))
    25:
    26:         return mean
0: mean = 0

使用-a标志将把变量添加到我们的显示列表中。如果我们转到下一行,你可以看到平均值的值是如何显示在底部的。

这里有一个小错误,说我们无法找到符号值。这是因为平均值还没有被定义。你可以看到,当我们在第21行时,这个错误就消失了,因为mean已经被定义了。

(dlv) next
> main.calcMean() ./main.go:22 (PC: 0x496b44)
    17: }
    18:
    19: func calcMean(nums []float64) float64 {
    20:         mean := 0.0
    21:         for _, num := range nums {
=>  22:                 mean += num
    23:         }
    24:         mean /= float64(len(nums))
    25:
    26:         return mean
    27: }
0: mean = 0
(dlv) next
> main.calcMean() ./main.go:21 (PC: 0x496b56)
    16:         fmt.Println("mean2:", mean2)
    17: }
    18:
    19: func calcMean(nums []float64) float64 {
    20:         mean := 0.0
=>  21:         for _, num := range nums {
    22:                 mean += num
    23:         }
    24:         mean /= float64(len(nums))
    25:
    26:         return mean
0: mean = 0.1
(dlv) next
> main.calcMean() ./main.go:22 (PC: 0x496b44)
    17: }
    18:
    19: func calcMean(nums []float64) float64 {
    20:         mean := 0.0
    21:         for _, num := range nums {
=>  22:                 mean += num
    23:         }
    24:         mean /= float64(len(nums))
    25:
    26:         return mean
    27: }
0: mean = 0.1
(dlv) next
> main.calcMean() ./main.go:21 (PC: 0x496b56)
    16:         fmt.Println("mean2:", mean2)
    17: }
    18:
    19: func calcMean(nums []float64) float64 {
    20:         mean := 0.0
=>  21:         for _, num := range nums {
    22:                 mean += num
    23:         }
    24:         mean /= float64(len(nums))
    25:
    26:         return mean
0: mean = 0.30000000000000004
(dlv) next
> main.calcMean() ./main.go:22 (PC: 0x496b44)
    17: }
    18:
    19: func calcMean(nums []float64) float64 {
    20:         mean := 0.0
    21:         for _, num := range nums {
=>  22:                 mean += num
    23:         }
    24:         mean /= float64(len(nums))
    25:
    26:         return mean
    27: }
0: mean = 0.30000000000000004
(dlv) next
> main.calcMean() ./main.go:21 (PC: 0x496b56)
    16:         fmt.Println("mean2:", mean2)
    17: }
    18:
    19: func calcMean(nums []float64) float64 {
    20:         mean := 0.0
=>  21:         for _, num := range nums {
    22:                 mean += num
    23:         }
    24:         mean /= float64(len(nums))
    25:
    26:         return mean
0: mean = 0.6000000000000001
(dlv) next
> main.calcMean() ./main.go:22 (PC: 0x496b44)
    17: }
    18:
    19: func calcMean(nums []float64) float64 {
    20:         mean := 0.0
    21:         for _, num := range nums {
=>  22:                 mean += num
    23:         }
    24:         mean /= float64(len(nums))
    25:
    26:         return mean
    27: }
0: mean = 0.6000000000000001
(dlv) next
> main.calcMean() ./main.go:21 (PC: 0x496b56)
    16:         fmt.Println("mean2:", mean2)
    17: }
    18:
    19: func calcMean(nums []float64) float64 {
    20:         mean := 0.0
=>  21:         for _, num := range nums {
    22:                 mean += num
    23:         }
    24:         mean /= float64(len(nums))
    25:
    26:         return mean
0: mean = 1
(dlv) next
> main.calcMean() ./main.go:22 (PC: 0x496b44)
    17: }
    18:
    19: func calcMean(nums []float64) float64 {
    20:         mean := 0.0
    21:         for _, num := range nums {
=>  22:                 mean += num
    23:         }
    24:         mean /= float64(len(nums))
    25:
    26:         return mean
    27: }
0: mean = 1
(dlv) next
> main.calcMean() ./main.go:21 (PC: 0x496b56)
    16:         fmt.Println("mean2:", mean2)
    17: }
    18:
    19: func calcMean(nums []float64) float64 {
    20:         mean := 0.0
=>  21:         for _, num := range nums {
    22:                 mean += num
    23:         }
    24:         mean /= float64(len(nums))
    25:
    26:         return mean
0: mean = 1.5
(dlv) next
> main.calcMean() ./main.go:24 (PC: 0x496b65)
    19: func calcMean(nums []float64) float64 {
    20:         mean := 0.0
    21:         for _, num := range nums {
    22:                 mean += num
    23:         }
=>  24:         mean /= float64(len(nums))
    25:
    26:         return mean
    27: }
0: mean = 1.5

我很抱歉输出量太长,但我真的想向你展示每一次迭代的平均值是如何变化的。你可以看到平均数在不断增加。

(dlv) next
> main.calcMean() ./main.go:26 (PC: 0x496b87)
    21:         for _, num := range nums {
    22:                 mean += num
    23:         }
    24:         mean /= float64(len(nums))
    25:
=>  26:         return mean
    27: }
0: mean = 0.3

除法后的平均数等于0.3,所以这个函数在输入n1的情况下如期工作。让我们看看当我们输入n2时会发生什么。

(dlv) next
> main.main() ./main.go:12 (PC: 0x4968c5)
Values returned:
        ~r0: 0.3
     7:
     8: func main() {
     9:         n1 := []float64{0.1, 0.2, 0.3, 0.4, 0.5}
    10:         n2 := []float64{math.NaN(), 0.2, 0.3, 0.4, 0.5}
    11:
=>  12:         mean1 := calcMean(n1)
    13:         mean2 := calcMean(n2)
    14:
    15:         fmt.Println("mean1:", mean1)
    16:         fmt.Println("mean2:", mean2)
    17: }
0: mean = error could not find symbol value for mean
(dlv) display -d 0
(dlv)

当我们碰到返回线时,我们就离开了范围。我们需要从显示列表中删除mean,因为我们不再跟踪它了。运行display -d,在它后面加上列表中变量的id。在我们的例子中,它是0。

(dlv) next
> main.main() ./main.go:13 (PC: 0x4968cb)
     8: func main() {
     9:         n1 := []float64{0.1, 0.2, 0.3, 0.4, 0.5}
    10:         n2 := []float64{math.NaN(), 0.2, 0.3, 0.4, 0.5}
    11:
    12:         mean1 := calcMean(n1)
=>  13:         mean2 := calcMean(n2)
    14:
    15:         fmt.Println("mean1:", mean1)
    16:         fmt.Println("mean2:", mean2)
    17: }
    18:
(dlv) step
> main.calcMean() ./main.go:19 (PC: 0x496ac0)
    14:
    15:         fmt.Println("mean1:", mean1)
    16:         fmt.Println("mean2:", mean2)
    17: }
    18:
=>  19: func calcMean(nums []float64) float64 {
    20:         mean := 0.0
    21:         for _, num := range nums {
    22:                 mean += num
    23:         }
    24:         mean /= float64(len(nums))
(dlv) print nums
[]float64 len: 5, cap: 5, [NaN,0.2,0.3,0.4,0.5]

再一次,我们进入calcMean函数。我们打印nums并检查输入是否符合预期。

dlv) display -a mean
0: mean = error could not find symbol value for mean
(dlv) next
> main.calcMean() ./main.go:20 (PC: 0x496ae5)
    15:         fmt.Println("mean1:", mean1)
    16:         fmt.Println("mean2:", mean2)
    17: }
    18:
    19: func calcMean(nums []float64) float64 {
=>  20:         mean := 0.0
    21:         for _, num := range nums {
    22:                 mean += num
    23:         }
    24:         mean /= float64(len(nums))
    25:
0: mean = error could not find symbol value for mean
(dlv) next
> main.calcMean() ./main.go:21 (PC: 0x496aeb)
    16:         fmt.Println("mean2:", mean2)
    17: }
    18:
    19: func calcMean(nums []float64) float64 {
    20:         mean := 0.0
=>  21:         for _, num := range nums {
    22:                 mean += num
    23:         }
    24:         mean /= float64(len(nums))
    25:
    26:         return mean
0: mean = 0

我们使用display命令来跟踪平均值。

(dlv) next
> main.calcMean() ./main.go:22 (PC: 0x496b44)
    17: }
    18:
    19: func calcMean(nums []float64) float64 {
    20:         mean := 0.0
    21:         for _, num := range nums {
=>  22:                 mean += num
    23:         }
    24:         mean /= float64(len(nums))
    25:
    26:         return mean
    27: }
0: mean = 0
(dlv) next
> main.calcMean() ./main.go:21 (PC: 0x496b56)
    16:         fmt.Println("mean2:", mean2)
    17: }
    18:
    19: func calcMean(nums []float64) float64 {
    20:         mean := 0.0
=>  21:         for _, num := range nums {
    22:                 mean += num
    23:         }
    24:         mean /= float64(len(nums))
    25:
    26:         return mean
0: mean = NaN
(dlv) next
> main.calcMean() ./main.go:22 (PC: 0x496b44)
    17: }
    18:
    19: func calcMean(nums []float64) float64 {
    20:         mean := 0.0
    21:         for _, num := range nums {
=>  22:                 mean += num
    23:         }
    24:         mean /= float64(len(nums))
    25:
    26:         return mean
    27: }
0: mean = NaN
(dlv) next
> main.calcMean() ./main.go:21 (PC: 0x496b56)
    16:         fmt.Println("mean2:", mean2)
    17: }
    18:
    19: func calcMean(nums []float64) float64 {
    20:         mean := 0.0
=>  21:         for _, num := range nums {
    22:                 mean += num
    23:         }
    24:         mean /= float64(len(nums))
    25:
    26:         return mean
0: mean = NaN

这就是问题所在。请注意,当我们加入nums的第一个元素时,mean就变成了NaN。从那时起,无论我们向它添加什么,平均值都是NaN。这显然是一种不可取的行为。我们要忽略NaN值,在没有NaN值的情况下计算平均值。

使用exit退出调试器。

让我们回到我们的代码中,将calcMean的代码更新为以下内容。

func calcMean(nums []float64) float64 {
    mean := 0.0
    nanCount := 0
    for _, num := range nums {
        if math.IsNaN(num) {
            nanCount++
            continue
        }
        mean += num
    }
    mean /= float64(len(nums) - nanCount)
    return mean
}

如果我们遇到一个NaN值,我们将nanCount增加1,并通过使用continue跳到下一个迭代。在计算平均值时,我们用nums的长度除以nums的长度再减去NaN的数量。

让我们看看我们的代码现在是如何运行的。

$ go run main.go
mean1: 0.3
mean2: 0.35

很好!我们成功地调试了我们的代码。

总结

我们学会了如何使用Delve来调试我们的代码,就像一个专家。当然,上面的例子可以通过在calcMean的循环中添加一个print语句来轻松调试。然而,当你在旅途中遇到讨厌的bug时,你会感谢自己阅读这篇文章并知道如何使用调试器。Delve提供了更多的命令,你可以在其文档中查看。上一次,我们学习了如何写测试来防止bug。当bug出现时,我们现在知道如何使用调试器来粉碎它们。