编程语言使用Delve代替Println来参与调试

    作者:Gaurav Kamathe更新于: 2020-08-07 18:16:35

    大神带你学编程,欢迎选课

    使用Delve代替Println来调试Go程序.编程语言往往使程序员能够比使用机器语言更准确地表达他们所想表达的目的。对那些从事计算机科学的人来说,懂得程序设计语言是十分重要的,因为在当今所有的计算都需要程序设计语言才能完成。

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

    编程语言使用Delve代替Println来参与调试_编程语言_操作系统_Linux_课课家

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

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

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

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

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

    Delve 介绍

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

    它自己的文档中写道:

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

    让我们来近距离看一下。

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

    1. $ cat/etc/fedora-release
    2. Fedora release 30(Thirty)
    3. $
    4. $ go version
    5. go version go1.12.17 linux/amd64
    6. $

    Golang 安装

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

    1. $ dnf install golang.x86_64

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

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

    1. $ go env|grep GOPATH
    2. GOPATH="/home/user/go"
    3. $
    4. $ go env|grep GOBIN
    5. GOBIN="/home/user/go/gobin"
    6. $

    Delve 安装

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

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

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

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

    1. $ ls-l $HOME/go
    2. total 8
    3. drwxrwxr-x.2 user user 4096May2519:11 bin
    4. drwxrwxr-x.4 user user 4096May2519:21 src
    5. $
    6. $ ls-l ~/go/bin/
    7. total 19596
    8. -rwxrwxr-x.1 user user 20062654May2519:17 dlv
    9. $

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

    1. $ which dlv
    2. ~/go/bin/dlv
    3. $
    4. $ dlv version
    5. DelveDebugger
    6. Version:1.4.1
    7. Build: $Id: bda606147ff48b58bde39e20b9e11378eaa4db46 $
    8. $

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

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

    1. $ pwd
    2. /home/user/go/gobin
    3. $
    4. $ cat hello.go
    5. package main
    6.  
    7. import"fmt"
    8.  
    9. func main(){
    10.         fmt.Println("Hello, world!")
    11. }
    12. $

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

    1. $ go build hello.go
    2. $
    3. $ ls-l hello
    4. -rwxrwxr-x.1 user user 1997284May2612:13 hello
    5. $
    6. $ ./hello
    7. Hello, world!
    8. $

    在 Delve 中加载程序

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

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

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

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

    1. $ ls-l
    2. total 4
    3. -rw-rw-r--.1 user user 74Jun  411:48 hello.go
    4. $
    5. $ dlv debug
    6. Type'help'forlist of commands.
    7. (dlv)

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

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

    使用 exec 参数

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

    1. $ ls-l
    2. total 4
    3. -rw-rw-r--.1 user user 74Jun  411:48 hello.go
    4. $
    5. $ go build hello.go
    6. $
    7. $ ls-l
    8. total 1956
    9. -rwxrwxr-x.1 user user 1997284Jun  411:54 hello
    10. -rw-rw-r--.1 user user      74Jun  411:48 hello.go
    11. $
    12. $ dlv exec./hello
    13. Type'help'forlist of commands.
    14. (dlv)

    查看 delve 帮助信息

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

    1. (dlv) help
    2. The following commands are available:
    3.  
    4. Running the program:
    5.  
    6. Manipulating breakpoints:
    7.  
    8. Viewing program variables and memory:
    9.  
    10. Listingand switching between threads and goroutines:
    11.  
    12. Viewing the call stack and selecting frames:
    13.  
    14. Other commands:
    15.  
    16. Type help followed by a command for full documentation.
    17. (dlv)

    设置断点

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

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

    1. (dlv)break main.main
    2. Breakpoint1set at 0x4a228ffor main.main()./hello.go:5
    3. (dlv) breakpoints
    4. Breakpoint runtime-fatal-throw at 0x42c410for runtime.fatalthrow()/usr/lib/golang/src/runtime/panic.go:663(0)
    5. Breakpoint unrecovered-panic at 0x42c480for runtime.fatalpanic()/usr/lib/golang/src/runtime/panic.go:690(0)
    6.         print runtime.curg._panic.arg
    7. Breakpoint1 at 0x4a228ffor main.main()./hello.go:5(0)
    8. (dlv)

    程序继续执行

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

    1. (dlv)continue
    2. > main.main()./hello.go:5(hits goroutine(1):1 total:1)(PC:0x4a228f)
    3.      1:package main
    4.      2:
    5.      3:import"fmt"
    6.      4:
    7. =>   5:      func main(){
    8.      6:         fmt.Println("Hello, world!")
    9.      7:}
    10. (dlv)next
    11. > main.main()./hello.go:6(PC:0x4a229d)
    12.      1:package main
    13.      2:
    14.      3:import"fmt"
    15.      4:
    16.      5: func main(){
    17. =>   6:              fmt.Println("Hello, world!")
    18.      7:}
    19. (dlv)next
    20. Hello, world!
    21. > main.main()./hello.go:7(PC:0x4a22ff)
    22.      2:
    23.      3:import"fmt"
    24.      4:
    25.      5: func main(){
    26.      6:         fmt.Println("Hello, world!")
    27. =>   7:      }
    28. (dlv)

    退出 Delve

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

    1. (dlv) quit
    2. $

    Delve 的其他功能

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

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

    1. $ cat functions.go
    2. package main
    3.  
    4. import"fmt"
    5.  
    6. func add(x int, y int)int{
    7.         return x + y
    8. }
    9.  
    10. func main(){
    11.         fmt.Println(add(42,13))
    12. }
    13. $

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

    1. $ go build functions.go  &&./functions
    2. 55
    3. $

    进入函数

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

    1. $ dlv debug
    2. Type'help'forlist of commands.
    3. (dlv)break main.main
    4. Breakpoint1set at 0x4a22b3for main.main()./functions.go:9
    5. (dlv) c
    6. > main.main()./functions.go:9(hits goroutine(1):1 total:1)(PC:0x4a22b3)
    7.      4:
    8.      5: func add(x int, y int)int{
    9.      6:         return x + y
    10.      7:}
    11.      8:
    12. =>   9:      func main(){
    13.     10:         fmt.Println(add(42,13))
    14.     11:}
    15. (dlv)next
    16. > main.main()./functions.go:10(PC:0x4a22c1)
    17.      5: func add(x int, y int)int{
    18.      6:         return x + y
    19.      7:}
    20.      8:
    21.      9: func main(){
    22. =>  10:              fmt.Println(add(42,13))
    23.     11:}
    24. (dlv) step
    25. > main.add()./functions.go:5(PC:0x4a2280)
    26.      1:package main
    27.      2:
    28.      3:import"fmt"
    29.      4:
    30. =>   5:      func add(x int, y int)int{
    31.      6:         return x + y
    32.      7:}
    33.      8:
    34.      9: func main(){
    35.     10:         fmt.Println(add(42,13))
    36. (dlv)

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

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

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

    查看当前的栈信息

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

    1. (dlv) stack
    2. 0  0x00000000004a2280in main.add
    3.    at ./functions.go:5
    4. 1  0x00000000004a22d7in main.main
    5.    at ./functions.go:10
    6. 2  0x000000000042dd1fin runtime.main
    7.    at /usr/lib/golang/src/runtime/proc.go:200
    8. 3  0x0000000000458171in runtime.goexit
    9.    at /usr/lib/golang/src/runtime/asm_amd64.s:1337
    10. (dlv)

    在帧之间跳转

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

    1. (dlv) frame 0
    2. > main.add()./functions.go:5(hits goroutine(1):1 total:1)(PC:0x4a2280)
    3. Frame0:./functions.go:5(PC:4a2280)
    4.      1:package main
    5.      2:
    6.      3:import"fmt"
    7.      4:
    8. =>   5:      func add(x int, y int)int{
    9.      6:         return x + y
    10.      7:}
    11.      8:
    12.      9: func main(){
    13.     10:         fmt.Println(add(42,13))
    14. (dlv) frame 1
    15. > main.add()./functions.go:5(hits goroutine(1):1 total:1)(PC:0x4a2280)
    16. Frame1:./functions.go:10(PC:4a22d7)
    17.      5: func add(x int, y int)int{
    18.      6:         return x + y
    19.      7:}
    20.      8:
    21.      9: func main(){
    22. =>  10:              fmt.Println(add(42,13))
    23.     11:}
    24. (dlv)

    打印函数参数

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

    1. (dlv) args
    2. x =42
    3. y =13
    4. ~r2 =824633786832
    5. (dlv)

    查看反汇编码

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

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

    单步退出函数

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

    1. (dlv) stepout
    2. > main.main()./functions.go:10(PC:0x4a22d7)
    3. Values returned:
    4.         ~r2:55
    5.  
    6.      5: func add(x int, y int)int{
    7.      6:         return x + y
    8.      7:}
    9.      8:
    10.      9: func main(){
    11. =>  10:              fmt.Println(add(42,13))
    12.     11:}
    13. (dlv)

    打印变量信息

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

    1. $ cat variables.go
    2. package main
    3.  
    4. import"fmt"
    5.  
    6. var i, j int=1,2
    7.  
    8. func main(){
    9.         var c, Python, java =true,false,"no!"
    10.         fmt.Println(i, j, c, python, java)
    11. }
    12. $
    13.  
    14. $ go build variables.go &&;./variables
    15. 12truefalseno!
    16. $

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

    1. (dlv)print c
    2. true
    3. (dlv)print java
    4. "no!"
    5. (dlv)

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

    1. (dlv) locals
    2. python =false
    3. c =true
    4. java ="no!"
    5. (dlv)

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

    1. (dlv)whatis python
    2. bool
    3. (dlv)whatis c
    4. bool
    5. (dlv)whatis java
    6. string
    7. (dlv)

    总结

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

    编程语言原本是被设计成专门使用在计算机上的,但它们也可以用来定义算法或者数据结构。正是因为如此,程序员才会试图使程序代码更容易阅读。

课课家教育

未登录