文中实例参考 Reading stack traces in Go
0x00 前言
调试程序有两大门派:日志派和 Debug 派,没有高下,只要能解决问题。Golang 的 Panic 输出和其语言风格一样,一点多余的内容都不会输出。有时 Panic 后,通过其打印的 Stack Traces 信息可以很快的定位问题,尤其是比较复杂的业务场景中。本文详细分析了 Golang Panic 的 Stack Traces 信息。
0x01 Panic 默认输出格式分析
通过一个例子来触发 panic,然后观察打印信息:
func main() {
iPanic(3)
}
func iPanic(i int) {
if i > 0 {
iPanic(i - 1)
}
panic("panic here")
}
信息输出为:
panic: panic here
goroutine 1 [running]:
main.iPanic(0x0)
/tmp/hello.go:11 +0x56
main.iPanic(0x1)
/tmp/hello.go:9 +0x3a
main.iPanic(0x2)
/tmp/hello.go:9 +0x3a
main.iPanic(0x3)
/tmp/hello.go:9 +0x3a
main.main()
/tmp/hello.go:4 +0x31
exit status 2
这个输出信息和 GDB 的 Back Trace 很像,只是 Golang 中以 Goroutine 为单位,默认情况下只打印引起 Panic 的 Goroutine 所在的 Stack Traces,全部打印可以使用 runtime.Stack()
修改第二个参数来操作。
每一条记录包含如下信息:
- 包名.函数名(参数)
main.iPanic(0x0)
- 文件:行数
/tmp/hello.go:11
- 当前函数在 Stack 中的相对位置
+0x56
Panic 时这个 Goroutine 的 Stack 在内存中的结构如下所示:
Function call | Relative position |
---|---|
main.iPanic(0x0) | +0x56 |
main.iPanic(...) | +0x3a |
main.main() | +0x31 |
Bottom of the stack | 0x00 |
0x02 各种数据类型的 Stack Traces 格式
通常使用打印 Stack Traces 信息的目的有两个:
- 找到 Panic 的发生位置和 Stack Frame 结构;
- 调试函数调用的参数细节;
第一个目的通过整体输出可以容易识别,但是第二个目的相对于 GDB 的使用习惯来说比较不友好,因为 GDB 会结合编译时插入的 .debug 段让 Back Trace 信息输出中的函数调用参数部分非常适合阅读,但是 Golang 的 Stack Trace 信息中的函数调用参数部分却相对比较晦涩,需要根据具体的参数类型进行区分。下文将对 Golang 中每种数据类型作为参数时对应的 Stack Trace 打印信息进行分析。
Case 01 忽略输出
Stack Traces 的参数列表中,如果所有的参数都未被使用或者只是在 fmt.Print()
中未作修改使用,那么参数列表将不会被打印,而是通过 func(...)
的形式打印,例如下面这段:
package main
import "fmt"
func main() {
iPanic(5)
}
func iPanic(i int) {
fmt.Println(i)
panic(i)
}
5
panic: 5
goroutine 1 [running]:
main.iPanic(...)
/tmp/hello.go:14 +0x114
main.main()
/tmp/hello.go:6 +0x31
exit status 2
Case 02 合并输出
Golang Stack Traces 不仅不是一个第一字段代表一个参数,而且会一个字段代表多个参数或者多个字段代表一个参数。来看下面的例子:
package main
import "fmt"
func main() {
iPanic(true, 'b', 'r')
}
func iPanic(bo bool, by byte, ru rune) {
fmt.Println(by + 1)
panic("panic here")
}
99
panic: panic here
goroutine 1 [running]:
main.iPanic(0x7200016201)
/tmp/hello.go:11 +0xa9
main.main()
/tmp/hello.go:6 +0x37
exit status 2
可以看出,iPanic
函数的三个参数在 Stack Traces 输出中被合并成了一个字段输出。0x7200016201
中 0x62='b'
, 0x72='r'
, 0x01=true
,这种将参数进行 Encode 的操作确实比较晦涩。一般看来,数字类型的参数(包括 bool
byte
rune
) 如果连续出现,会被编码输出,具体编码规则还需要进一步分析。
Case 03 常规输出
下面列举 Golang 非基本类型作为参数时在 Stack Trace 中的形态,与各种类型的底层数据结构基本相同。
类型名称 | 参数域数量 | 参数域说明 |
---|---|---|
string | 2 | 指针 长度 |
slice | 3 | 指针 长度 容量 |
map | 1 | 指针 |
chan | 1 | 指针 |
interface | 2 | 类型指针 值指针 |
pointer | 1 | 指针 |
func | 1 | 指针 |
nil | 1 | 0x0 |
Case 04 结构体输出
Struct 是字段和嵌入结构和接口的集合,当通过按值引用的方式使用结构体作为参数时,Stack Traces 将按照结构体的内部结构来打印。
package main
import "fmt"
type A struct {
i int
s string
}
func main() {
iPanic(A{i: 50})
}
func iPanic(a A) {
fmt.Println(a.i + 1)
panic("panic here")
}
51
panic: panic here
goroutine 1 [running]:
main.iPanic(0x32, 0x0, 0x0)
/tmp/hello.go:16 +0xab
main.main()
/tmp/hello.go:11 +0x39
exit status 2
Case 05 方法输出
Golang 中特有的以一个 Struct 作为 Receiver 的方式我们暂且称为方法(Method),Method 的 Stack Trace 输出与 Function 的区别是,先打印 Receiver 再打印 Method 参数。Method 的 Stack Traces 输出根据 Receiver 的类型分为两种情况:
当 Method 的 Receiver 为 Value Receiver 时:
package main
import "fmt"
type A struct {
i int
s string
}
func main() {
A{i: 50}.iPanic(true)
}
func (a A) iPanic(b bool) {
fmt.Println(a.i + 1)
panic("panic here")
}
51
panic: panic here
goroutine 1 [running]:
main.A.iPanic(0x32, 0x0, 0x0, 0xc00001a001)
/tmp/hello.go:16 +0xab
main.main()
/tmp/hello.go:11 +0x3e
exit status 2
当 Method 的 Receiver 为 Pointer Receiver 时:
package main
import "fmt"
type A struct {
i int
s string
}
func main() {
(&A{i: 50}).iPanic(true)
}
func (a *A) iPanic(b bool) {
fmt.Println(a.i + 1)
panic("panic here")
}
51
panic: panic here
goroutine 1 [running]:
main.(*A).iPanic(0xc0000c7f60, 0x1)
/tmp/hello.go:16 +0xae
main.main()
/tmp/hello.go:11 +0x56
exit status 2