在Go语言中有一些调试技巧能帮助我们快速找到问题,有时候你想尽可能多的记录异常但仍觉得不够,搞清楚堆栈的意义有助于定位Bug或者记录更完整的信息。
本文将讨论堆栈跟踪信息以及如何在堆栈中识别函数所传递的参数。
Functions
先从这段代码开始:
Listing 1
01 package main
02
03 func main() {
04 slice := make([]string, 2, 4)
05 Example(slice, "hello", 10)
06 }
07
08 func Example(slice []string, str string, i int) {
09 panic("Want stack trace")
10 }
Example函数定义了3个参数,1个string类型的slice, 1个string和1个integer, 并且抛出了panic,运行这段代码可以看到这样的结果:
Listing 2
Panic: Want stack trace
goroutine 1 [running]:
main.Example(0x2080c3f50, 0x2, 0x4, 0x425c0, 0x5, 0xa)
/Users/bill/Spaces/Go/Projects/src/github.com/goinaction/code/
temp/main.go:9 +0x64
main.main()
/Users/bill/Spaces/Go/Projects/src/github.com/goinaction/code/
temp/main.go:5 +0x85
goroutine 2 [runnable]:
runtime.forcegchelper()
/Users/bill/go/src/runtime/proc.go:90
runtime.goexit()
/Users/bill/go/src/runtime/asm_amd64.s:2232 +0x1
goroutine 3 [runnable]:
runtime.bgsweep()
/Users/bill/go/src/runtime/mgc0.go:82
runtime.goexit()
/Users/bill/go/src/runtime/asm_amd64.s:2232 +0x1
堆栈信息中显示了在panic抛出这个时间所有的goroutines状态,发生的panic的goroutine会显示在最上面。
Listing 3
01 goroutine 1 [running]:
02 main.Example(0x2080c3f50, 0x2, 0x4, 0x425c0, 0x5, 0xa)
/Users/bill/Spaces/Go/Projects/src/github.com/goinaction/code/
temp/main.go:9 +0x64
03 main.main()
/Users/bill/Spaces/Go/Projects/src/github.com/goinaction/code/
temp/main.go:5 +0x85
第1行显示最先发出panic的是goroutine 1, 第二行显示panic位于main.Example中, 并能定位到该行代码,在本例中第9行引发了panic。
下面我们关注参数是如何传递的:
Listing 4
// Declaration
main.Example(slice []string, str string, i int)
// Call to Example by main.
slice := make([]string, 2, 4)
Example(slice, "hello", 10)
// Stack trace
main.Example(0x2080c3f50, 0x2, 0x4, 0x425c0, 0x5, 0xa)
这里展示了在main中带参数调用Example函数时的堆栈信息,比较就能发现两者的参数数量并不相同,Example定义了3个参数,堆栈中显示了6个参数。现在的关键问题是我们要弄清楚它们是如何匹配的。
第1个参数是string类型的slice,我们知道在Go语言中slice是引用类型,即slice变量结构会包含三个部分:指针、长度(Lengthe)、容量(Capacity)
Listing 5
// Slice parameter value
slice := make([]string, 2, 4)
// Slice header values
Pointer: 0x2080c3f50
Length: 0x2
Capacity: 0x4
// Declaration
main.Example(slice []string, str string, i int)
// Stack trace
main.Example(0x2080c3f50, 0x2, 0x4, 0x425c0, 0x5, 0xa)
因此,前面3个参数会匹配slice, 如下图所示:
Figure 1
figure provided by Georgi Knox
我们现在来看第二个参数,它是string类型,string类型也是引用类型,它包括两部分:指针、长度。
Listing 6
// String parameter value
"hello"
// String header values
Pointer: 0x425c0
Length: 0x5
// Declaration
main.Example(slice []string, str string, i int)
// Stack trace
main.Example(0x2080c3f50, 0x2, 0x4, 0x425c0, 0x5, 0xa)
可以确定,堆栈信息中第4、5两个参数对应代码中的string参数,如下图所示:
Figure 2
figure provided by Georgi Knox
最后一个参数integer是single word值。
Listing 7
// Integer parameter value
10
// Integer value
Base 16: 0xa
// Declaration
main.Example(slice []string, str string, i int)
// Stack trace
main.Example(0x2080c3f50, 0x2, 0x4, 0x425c0, 0x5, 0xa)
现在我们可以匹配代码中的参数到堆栈信息了。
Figure 3
figure provided by Georgi Knox
Methods
如果我们将Example作为结构体的方法会怎么样呢?
Listing 8
01 package main
02
03 import "fmt"
04
05 type trace struct{}
06
07 func main() {
08 slice := make([]string, 2, 4)
09
10 var t trace
11 t.Example(slice, "hello", 10)
12 }
13
14 func (t *trace) Example(slice []string, str string, i int) {
15 fmt.Printf("Receiver Address: %p\n", t)
16 panic("Want stack trace")
17 }
如上所示修改代码,将Example定义为trace的方法,并通过trace的实例t来调用Example。
再次运行程序,会发现堆栈信息有一点不同:
Listing 9
Receiver Address: 0x1553a8
panic: Want stack trace
01 goroutine 1 [running]:
02 main.(*trace).Example(0x1553a8, 0x2081b7f50, 0x2, 0x4, 0xdc1d0, 0x5, 0xa)
/Users/bill/Spaces/Go/Projects/src/github.com/goinaction/code/
temp/main.go:16 +0x116
03 main.main()
/Users/bill/Spaces/Go/Projects/src/github.com/goinaction/code/
temp/main.go:11 +0xae
首先注意第2行的方法调用使用了pointer receiver,在package名字和方法名之间多出了"*trace"字样。另外,参数列表的第1个参数标明了结构体(t)地址。我们从堆栈信息中看到了内部实现细节。
Packing
如果有多个参数可以填充到一个single word, 则这些参数值会合并打包:
Listing 10
01 package main
02
03 func main() {
04 Example(true, false, true, 25)
05 }
06
07 func Example(b1, b2, b3 bool, i uint8) {
08 panic("Want stack trace")
09 }
这个例子修改Example函数为4个参数:3个bool型和1个八位无符号整型。bool值也是用8个bit表示,所以在32位和64位架构下,4个参数可以合并为一个single word。
Listing 11
01 goroutine 1 [running]:
02 main.Example(0x19010001)
/Users/bill/Spaces/Go/Projects/src/github.com/goinaction/code/
temp/main.go:8 +0x64
03 main.main()
/Users/bill/Spaces/Go/Projects/src/github.com/goinaction/code/
temp/main.go:4 +0x32
这是本例的堆栈信息,看下图的具体分析:
Listing 12
// Parameter values
true, false, true, 25
// Word value
Bits Binary Hex Value
00-07 0000 0001 01 true
08-15 0000 0000 00 false
16-23 0000 0001 01 true
24-31 0001 1001 19 25
// Declaration
main.Example(b1, b2, b3 bool, i uint8)
// Stack trace
main.Example(0x19010001)
以上展示了参数值是如何匹配到4个参数的。当我们看到堆栈信息中包括十六进制值,需要知道这些值是如何传递的。
英文原文链接【Go, the unwritten parts】 发表于2017/05/22 作者JBD是Go语言开发小组成员
检查程序的执行路径和当前状态是非常有用的调试手段。核心文件(core file)包含了一个运行进程的内存转储和状态。它主要是用来作为事后调试程序用的。它也可以被用来查看一个运行中的程序的状态。这两个使用场景使调试文件转储成为一个非常好的诊断手段。我们可以用这个方法来做事后诊断和分析线上的服务(production services)。
在这篇文章中,我们将用一个简单的hello world网站服务作为例子。在现实中,我们的程序很容易就会变得很复杂。分析核心转储给我们提供了一个机会去重构程序的状态并且查看只有在某些条件/环境下才能重现的案例。
作者注 : 这个调试流程只在Linux上可行。我不是很确定它是否在其它Unixs系统上工作。macOS对此还不支持。Windows现在也不支持。
在我们开始前,需要确保核心转储的ulimit设置在合适的范围。它的缺省值是0,意味着最大的核心文件大小是0。我通常在我的开发机器上将它设置成unlimited。使用以下命令:
接下来,你需要在你的机器上安装 delve 。
下面我们使用的 main.go 文件。它注册了一个简单的请求处理函数(handler)然后启动了HTTP服务。
让我们编译并生产二进制文件。
现在让我们假设,这个服务器出了些问题,但是我们并不是很确定问题的根源。你可能已经在程序里加了很多辅助信息,但还是无法从这些调试信息中找出线索。通常在这种情况下,当前进程的快照会非常有用。我们可以用这个快照深入查看程序的当前状态。
有几个方式来获取核心文件。你可能已经熟悉了奔溃转储(crash dumps)。它们是在一个程序奔溃的时候写入磁盘的核心转储。Go语言在缺省设置下不会生产奔溃转储。但是当你把 GOTRACEBACK 环境变量设置成“crash”,你就可以用 Ctrl+backslash 才触发奔溃转储。如下图所示:
上面的操作会使程序终止,将堆栈跟踪(stack trace)打印出来,并把核心转储文件写入磁盘。
另外个方法可以从一个运行的程序获得核心转储而不需要终止相应的进程。 gcore 可以生产核心文件而无需使运行中的程序退出。
根据上面的操作,我们获得了转储而没有终止对应的进程。下一步就是把核心文件加载进delve并开始分析。
差不多就这些。delve的常用操作都可以使用。你可以backtrace,list,查看变量等等。有些功能不可用因为我们使用的核心转储是一个快照而不是正在运行的进程。但是程序执行路径和状态全部可以访问。