使用Delve调试Go应用程序的方法

367 阅读11分钟

对调试器的需求

在任何编程语言中,最简单的调试形式是使用打印语句/日志和写到标准输出。这无疑是可行的,但当我们的应用程序的规模越来越大,逻辑越来越复杂时,就变得非常困难。将打印语句添加到应用程序的每条代码路径中并不容易。这时,调试器就派上用场了。调试器帮助我们使用断点和其他大量的功能来追踪程序的执行路径。Delve就是这样一个用于Go的调试器。在本教程中,我们将学习如何使用Delve对Go程序进行调试。

安装Delve

请确保你在一个不包含go.mod文件的目录中。我更喜欢我的Documents 目录。

cd ~/Documents/  

接下来,让我们设置GOBIN 环境变量。这个环境变量指定了安装Delve 二进制文件的位置。如果你已经设置了GOBIN ,请跳过此步骤。你可以通过运行下面的命令来检查GOBIN 是否被设置。

go env | grep GOBIN  

如果上面的命令打印出:GOBIN="" ,这意味着GOBIN 没有被设置。请运行export GOBIN=~/go/bin/ 命令来设置GOBIN。

让我们通过运行GOBIN ,把PATHexport PATH=$PATH:~/go/bin

macOS的情况下,需要Xcode命令行开发工具来运行Delve。请运行xcode-select --install 来安装命令行工具。Linux用户可以跳过这个步骤。

现在我们准备安装Delve 。请运行

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

来安装Delve。运行这个命令后,请通过运行dlv version 来测试你的安装。它将在安装成功后打印出Delve的版本。

Delve Debugger  
Version: 1.4.0  
Build: $Id: 67422e6f7148fa1efa0eac1423ab5594b223d93b  

启动Delve

让我们写一个简单的程序,然后用Delve开始调试它。

让我们用下面的命令为我们的样本程序创建一个目录。

mkdir ~/Documents/debugsample  

在我们刚刚创建的debugsample 目录内创建一个文件main.go ,内容如下。

package main

import (  
    "fmt"
)

func main() {  
    arr := []int{101, 95, 10, 188, 100}
    max := arr[0]
    for _, v := range arr {
        if v > max {
            max = v
        }
    }
    fmt.Printf("Max element is %d\n", max)
}

上面的程序将打印片断的最大元素arr运行上面的程序将输出。

Max element is 188  

我们现在准备对程序进行调试。让我们移动到debugsample目录cd ~/Documents/debugsample 。之后,输入下面的命令来启动Delve。

dlv debug  

上面的命令将开始调试当前目录下的main 。输入上述命令后,你可以看到终端已经变成了(dlv) 提示。如果你能看到这个变化,说明调试器已经成功启动,正在等待我们的命令:)。

让我们启动我们的第一个命令。

dlv 提示下,输入continue

(dlv) continue

continue 命令将运行程序,直到有一个断点或直到程序完成。由于我们没有定义任何断点,程序将运行到完成。

Max element is 188  
Process 1733 has exited with status 0  

如果你看到上面的输出,说明调试器已经运行,程序已经完成:)。但这对我们来说没有任何用处。让我们继续添加几个断点,看着调试器发挥它的魔力。

创建断点

断点会在指定的行上暂停程序的执行。当执行被暂停时,我们可以向调试器发送命令,打印变量的值,查看程序的堆栈跟踪,等等。

下面提供了创建断点的语法。

(dlv) break filename:lineno

上述命令将在文件filename 中的lineno 行创建一个断点。

让我们在第9行添加一个断点。main.go 的第9行。

(dlv) break main.go:9

当上述命令运行时,你可以看到输出,Process 1733 has exited with status 0实际上,这个断点并没有被添加。这是因为我们之前运行continue ,程序已经退出了,因为当时没有断点。让我们重新启动程序,再次尝试设置断点。

(dlv) restart
Process restarted with PID 2028  
(dlv) break main.go:9
Breakpoint 1 set at 0x10c16e4 for main.main() ./main.go:9  

restart 命令重新启动程序,然后用break 命令设置断点。上述输出结果证实,名称为1 的断点被设置在main.go的第9行。main.go中的第9行。

现在让我们把我们的程序continue ,检查调试器是否在断点处暂停程序。

(dlv) continue
> main.main() ./main.go:9 (hits goroutine(1):1 total:1) (PC: 0x10c16e4)
     4:        "fmt"
     5:    )
     6:    
     7:    func main() {
     8:        arr := []int{101, 95, 10, 188, 100}
=>   9:        max := arr[0]
    10:        for _, v := range arr {
    11:            if v > max {
    12:                max = v
    13:            }
    14:        }

在执行continue ,我们可以看到调试器在第9行暂停了我们的程序。这正是我们想要的:)。

列出断点

(dlv) breakpoints

上面的命令列出了当前程序的断点。

(dlv) breakpoints
Breakpoint runtime-fatal-throw at 0x102de10 for runtime.fatalthrow() /usr/local/Cellar/go/1.13.7/libexec/src/runtime/panic.go:820 (0)  
Breakpoint unrecovered-panic at 0x102de80 for runtime.fatalpanic() /usr/local/Cellar/go/1.13.7/libexec/src/runtime/panic.go:847 (0)  
    print runtime.curg._panic.arg
Breakpoint 1 at 0x10c16e4 for main.main() ./main.go:9 (1)  

你可能会惊讶地发现,除了我们添加的那个断点外,还有另外两个断点。另外两个断点是由delve添加的,以确保调试会话不会在出现未用*recover处理的运行时恐慌*时突然结束。

打印变量

该程序的执行在第9行暂停了。9。print 是用来打印变量值的命令。让我们使用print ,打印分片的第0个索引的元素arr

(dlv) print arr[0]

运行上述命令将打印101 ,它是分片arr 的第0个索引的元素。

请注意,如果我们试图打印max ,我们会得到一个垃圾值。

(dlv) print max
824634294736  

这是因为程序在执行第9行之前已经暂停了。这是因为程序在执行第9行之前已经暂停,因此打印max ,会打印出一些随机的垃圾值。为了打印max的实际值,我们应该移到程序的下一行。这可以用next 命令来完成。

移动到源代码中的下一行

(dlv) next

将会把调试器移到下一行,并且会输出。

> main.main() ./main.go:10 (PC: 0x10c16ee)
     5:    )
     6:    
     7:    func main() {
     8:        arr := []int{101, 95, 10, 188, 100}
     9:        max := arr[0]
=>  10:        for _, v := range arr {
    11:            if v > max {
    12:                max = v
    13:            }
    14:        }
    15:        fmt.Printf("Max element is %d\n", max)

现在如果我们尝试(dlv) print max ,我们可以看到输出101

next命令可以用来逐行浏览一个程序。

如果你继续输入next ,你可以看到调试器在程序中逐行走过。当第10行的for 循环的一次迭代结束后, 。10行的循环结束后,next 将带领我们进行下一次迭代,程序将最终终止。

打印表达式

print也可以用来评估表达式。例如,如果我们想找到max + 10 的值,就可以用print。

让我们在for 循环外添加另一个断点,在这里将完成max 的计算。

(dlv) break main.go:15

上面的命令在第15行添加了一个断点。15行添加了另一个断点,在这里max的计算已经完成。

输入continue ,程序将在此断点处停止。

print max+10命令将输出198

清除断点

clear是清除单个断点的命令,clearall是清除程序中所有断点的命令。

让我们首先列出我们应用程序中的断点。

(dlv) breakpoints

Breakpoint runtime-fatal-throw at 0x102de10 for runtime.fatalthrow() /usr/local/Cellar/go/1.13.7/libexec/src/runtime/panic.go:820 (0)  
Breakpoint unrecovered-panic at 0x102de80 for runtime.fatalpanic() /usr/local/Cellar/go/1.13.7/libexec/src/runtime/panic.go:847 (0)  
    print runtime.curg._panic.arg
Breakpoint 1 at 0x10c16e4 for main.main() ./main.go:9 (1)  
Breakpoint 2 at 0x10c1785 for main.main() ./main.go:15 (1)  

我们有两个断点,分别名为12

如果我们运行clear 1 ,它将删除断点1

(dlv) clear 1
Breakpoint 1 cleared at 0x10c16e4 for main.main() ./main.go:9  

如果我们运行clearall ,它将删除所有断点。我们只剩下一个名为2 的断点。

(dlv) clearall
Breakpoint 2 cleared at 0x10c1785 for main.main() ./main.go:15  

从上面的输出中,我们可以看到,剩下的一个断点也被清除了。如果我们现在执行continue 命令,程序将打印出max 的值并终止。

(dlv) continue
Max element is 188  
Process 3095 has exited with status 0  

踏入和踏出一个函数

我们可以用Delve来进入一个函数或退出一个函数。不要担心,如果现在没有意义的话:)。让我们试着借助一个例子来理解这个问题。

package main

import (  
    "fmt"
)

func max(arr []int) int {  
    max := arr[0]
    for _, v := range arr {
        if v > max {
            max = v
        }
    }
    return max
}
func main() {  
    arr := []int{101, 95, 10, 188, 100}
    m := max(arr)
    fmt.Printf("Max element is %d\n", m)
}

我修改了到目前为止我们一直在使用的程序,并将找到切片中最大元素的逻辑移到自己的函数中,命名为max

使用(dlv) q 退出Delve,用上面的程序替换main.go ,然后使用dlv debug 命令再次开始调试。

让我们在第18行添加一个断点,即调用max 函数的地方。

b是添加断点的简写。让我们使用这个。

(dlv) b main.go:18
(dlv) continue

我们已经在第18行添加了断点,并继续执行程序。运行上述命令将打印。

> main.main() ./main.go:18 (hits goroutine(1):1 total:1) (PC: 0x10c17ae)
    13:        }
    14:        return max
    15:    }
    16:    func main() {
    17:        arr := []int{101, 95, 10, 188, 100}
=>  18:        m := max(arr)
    19:        fmt.Printf("Max element is %d\n", m)
    20:    }

程序的执行在第18行暂停了。18行暂停,正如预期的那样。现在我们有两个选择。

  • 继续深入调试max 函数
  • 跳过max函数,转到下一行。

根据我们的要求,我们可以做任何一种。让我们来学习一下如何做这两件事。

首先,让我们跳过max函数,转到下一行。要做到这一点,你可以直接运行next ,调试器会自动移动到下一行。默认情况下,Delve不会深入到函数调用。

(dlv) next
> main.main() ./main.go:19 (PC: 0x10c17d3)
    14:        return max
    15:    }
    16:    func main() {
    17:        arr := []int{101, 95, 10, 188, 100}
    18:        m := max(arr)
=>  19:        fmt.Printf("Max element is %d\n", m)
    20:    }

你可以从上面的输出中看到,调试器已经移到了下一行。

输入continue ,程序将完成执行。

让我们来学习如何深入到max函数中去。

输入restartcontinue ,我们可以看到程序在已经存在的断点处再次暂停。

(dlv) restart
Process restarted with PID 5378  
(dlv) continue
> main.main() ./main.go:18 (hits goroutine(1):1 total:1) (PC: 0x10c17ae)
    13:        }
    14:        return max
    15:    }
    16:    func main() {
    17:        arr := []int{101, 95, 10, 188, 100}
=>  18:        m := max(arr)
    19:        fmt.Printf("Max element is %d\n", m)
    20:    }

现在输入step ,我们可以看到控制现在已经移到了max 函数中。

(dlv) step
> main.max() ./main.go:7 (PC: 0x10c1650)
     2:    
     3:    import (
     4:        "fmt"
     5:    )
     6:    
=>   7:    func max(arr []int) int {
     8:        max := arr[0]
     9:        for _, v := range arr {
    10:            if v > max {
    11:                max = v
    12:            }

输入next ,控件将移动到max 函数的第一行。

(dlv) next
> main.max() ./main.go:8 (PC: 0x10c1667)
     3:    import (
     4:        "fmt"
     5:    )
     6:    
     7:    func max(arr []int) int {
=>   8:        max := arr[0]
     9:        for _, v := range arr {
    10:            if v > max {
    11:                max = v
    12:            }
    13:        }

如果你继续输入next ,你就可以踏过max 函数的执行路径。

你可能想知道是否有可能不通过max 函数中的每一行就返回到main 。是的,使用stepout 命令可以做到这一点。

(dlv) stepout
> main.main() ./main.go:18 (PC: 0x10c17c9)
Values returned:  
    ~r1: 188

    13:        }
    14:        return max
    15:    }
    16:    func main() {
    17:        arr := []int{101, 95, 10, 188, 100}
=>  18:        m := max(arr)
    19:        fmt.Printf("Max element is %d\n", m)
    20:    }

一旦你输入stepout ,控件就会返回到main。现在你可以继续在main 中进行调试 :)。

打印堆栈跟踪

调试时需要的一个非常重要的功能是打印程序的当前堆栈跟踪。这对找出当前代码的执行路径很有用。stack 是用来打印当前堆栈跟踪的命令。

让我们清除所有的断点,在第11行添加一个新的断点。11行添加一个新的断点,并打印当前程序的堆栈跟踪。

(dlv) restart
(dlv) clearall
(dlv) b main.go:11
(dlv) continue

当程序在断点处暂停时,输入

(dlv) stack

它将输出程序的当前堆栈跟踪。

0  0x00000000010c16e8 in main.max  
   at ./main.go:11
1  0x00000000010c17c9 in main.main  
   at ./main.go:18
2  0x000000000102f754 in runtime.main  
   at /usr/local/Cellar/go/1.13.7/libexec/src/runtime/proc.go:203
3  0x000000000105acc1 in runtime.goexit  
   at /usr/local/Cellar/go/1.13.7/libexec/src/runtime/asm_amd64.s:1357

到目前为止,我们已经涵盖了基本命令,以帮助开始使用Delve调试你的应用程序。在接下来的教程中,我们将介绍Delve的高级功能,如调试goroutines,将调试器附加到现有进程,远程调试,以及从VSCode编辑器中使用Delve。

谢谢你的阅读。请留下您的评论和反馈。