Golang调试技巧
#go #调试 #softwareengineering

可以在此处找到本文的'代码':https://github.com/peter-mcconnell/petermcconnell.com/blob/main/assets/dummy/godebug/main.go

调试golang-上下文

这是我在程序不表现出我期望的方式时所能达到的工具是错误的。

有什么要求?

选择的调试器(对我来说)是dlv / delve。原因是本文的结尾。

安装delvehttps://github.com/go-delve/delve

git clone https://github.com/go-delve/delve
cd delve
go install github.com/go-delve/delve/cmd/dlv

有以下信息:

  • 我想设置断点的代码中的1个或更多点的列表
    • 文件名和行号

示例程序

为了本文,我们将创建一个简单的应用程序来调试:

// main.go
package main

import "fmt"

func doubleit(val int) int {
        return val * 3  // should be * 2
}

func main() {
        fmt.Printf("doubleit 2: %d\n", doubleit(2))
        fmt.Printf("doubleit 4: %d\n", doubleit(4))
        fmt.Printf("doubleit 8: %d\n", doubleit(8))
}

当我们使用go run main.go运行时,我们希望它使我们通过的数字加倍,但出于某些原因我们得到了不同的结果。

我们还需要一个go.mod才能运行,因此也可以从该目录中执行go mod init

调试它

从项目目录开始

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

现在设置一些断点。对于此示例,我们要说我们要调试我们的doubleit方法 - 第一行是在:7

dlv debug
Type 'help' for list of commands.
(dlv) b main.go:7
Breakpoint 1 set at 0x49c8bb for main.doubleit() ./main.go:7

当我们添加所有需要的断点时,我们可以指示程序以c
运行

dlv debug
Type 'help' for list of commands.
(dlv) b main.go:7
Breakpoint 1 set at 0x49c8bb for main.doubleit() ./main.go:7
(dlv) c
> main.doubleit() ./main.go:7 (hits goroutine(1):1 total:1) (PC: 0x49c8bb)
     2:package main
     3:
     4:import "fmt"
     5:
     6:func doubleit(val int) int {
=>   7: return val * 3 // should be * 2
     8:}
     9:
    10:func main() {
    11: fmt.Printf("doubleit 2: %d\n", doubleit(2))
    12: fmt.Printf("doubleit 4: %d\n", doubleit(4))
(dlv)

现在,我们已经使用附带的调试器运行了程序,并且已在我们设置的断点处停止执行。我们可以运行args,以查看哪些参数传递给方法:

(dlv) c
> main.doubleit() ./main.go:7 (hits goroutine(1):1 total:1) (PC: 0x49c8bb)
     2:package main
     3:
     4:import "fmt"
     5:
     6:func doubleit(val int) int {
=>   7: return val * 3 // should be * 2
     8:}
     9:
    10:func main() {
    11: fmt.Printf("doubleit 2: %d\n", doubleit(2))
    12: fmt.Printf("doubleit 4: %d\n", doubleit(4))
(dlv) args
val = 2
~r0 = 0

因此,在程序中的这一点上,当它被val值为2时,我们使用了doubleit方法。我们可以使用p打印此和其他变量:

(dlv) p val
2

我们甚至可以使用call从此来调用方法:

(dlv) call doubleit(6)
> main.doubleit() ./main.go:7 (hits goroutine(6):1 total:2) (PC: 0x49c8bb)
     2:package main
     3:
     4:import "fmt"
     5:
     6:func doubleit(val int) int {
=>   7: return val * 3 // should be * 2
     8:}
     9:
    10:func main() {
    11: fmt.Printf("doubleit 2: %d\n", doubleit(2))
    12: fmt.Printf("doubleit 4: %d\n", doubleit(4))
(dlv) args
val = 6

在上面的示例中,我们访问了自己的断点集,允许我们为call打印args

要浏览执行,我们可以按n进行执行点:

(dlv) n
> main.doubleit() ./main.go:6 (PC: 0x49c8a0)
     1:// main.go
     2:package main
     3:
     4:import "fmt"
     5:
=>   6:func doubleit(val int) int {
     7: return val * 3 // should be * 2
     8:}
     9:
    10:func main() {
    11: fmt.Printf("doubleit 2: %d\n", doubleit(2))
(dlv) n
> main.doubleit() ./main.go:7 (hits goroutine(6):2 total:3) (PC: 0x49c8bb)
     2:package main
     3:
     4:import "fmt"
     5:
     6:func doubleit(val int) int {
=>   7: return val * 3 // should be * 2
     8:}
     9:
    10:func main() {
    11: fmt.Printf("doubleit 2: %d\n", doubleit(2))
    12: fmt.Printf("doubleit 4: %d\n", doubleit(4))
(dlv) n

并使用bt查看回溯:

(dlv) bt
0  0x000000000049c8bb in main.doubleit
   at ./main.go:7
1  0x000000000046251f in debugCall256
   at :0
2  0x0000000000407484 in runtime.debugCallWrap2
   at /usr/local/go/src/runtime/debugcall.go:251
3  0x00000000004073b3 in runtime.debugCallWrap1
   at /usr/local/go/src/runtime/debugcall.go:203
4  0x0000000000464ca1 in runtime.goexit
   at /usr/local/go/src/runtime/asm_amd64.s:1594

要查看当前执行点附近的代码,只需按l

(dlv) l
> main.doubleit() ./main.go:7 (hits goroutine(6):3 total:4) (PC: 0x49c8bb)
     2:package main
     3:
     4:import "fmt"
     5:
     6:func doubleit(val int) int {
=>   7: return val * 3 // should be * 2
     8:}
     9:
    10:func main() {
    11: fmt.Printf("doubleit 2: %d\n", doubleit(2))
    12: fmt.Printf("doubleit 4: %d\n", doubleit(4))

当然,哪个表明我们很难找到逻辑错误,* 3而不是* 2

注意:您还可以在stdlib函数中设置断点(路径会根据您的设置而变化):

(dlv) b src/net/http/request.go:899
Breakpoint 1 set at 0x794599 for net/http.NewRequestWithContext() /usr/local/go./net/http/request.go:899

概括

上面的示例非常微不足道 - dlv和它的质量是在复杂的用途上,您甚至可能不知道输入和输出之间的方法,例如调试stdlib。就在本周,我使用dlv来确定为什么POST没有尊重307临时重定向 - 在检查时,使用dlv,我了解到body是否被忽略了,如果它是一个不认可的type https://github.com/golang/go/blob/master/src/net/http/request.go#L899。不得不在不调试器的情况下做到这一点会花费很多代码跳动 - 调试器为我处理了这一点,并允许我像我一样验证参数值。

为什么不gdb?

我知道有些人强烈认为gdb是用于调试GO代码的工具,但是鉴于Golang Docs本身鼓励您在GDB上使用Delve,我个人远离它:

请注意,在使用标准工具链构建的调试GO程序时,Delve是GDB的更好替代方法。它比GDB更好地了解GO运行时,数据结构和表达式。 Delve当前支持AMD64上的Linux,OSX和Windows。有关受支持平台的最新列表,请参见Delve文档。

例外情况可能是CGO的使用,但我现在将其排除在外。

来源:https://tip.golang.org/doc/gdb