问题由来
以前出现panic问题,总是习惯通过日志中给出的代码行,去“猜测”是哪个变量出了问题,
如果推断不出来,就多加入一些日志,重现panic,再继续定位。
昨天又遇到了panic的问题,看到屏幕上打印了很多堆栈日志,转念一想:
如果现网出现了panic但日志信息不够怎么办,总不能先加日志等下次重现后再定位吧?
然后尝试仔细阅读堆栈日志时,却多出了一些疑惑:
为什么定义是:
func Fun1(slice []string, t *Test, i int)
但堆栈日志显示:
main.Fun1(0xc000078f48, 0x2, 0x4, 0x0, 0x7)
/Users/leidingyu/workspace/go_proj/src/git.code.oa.com/SNG_EDU_GO_SVR/unittest/main.go:33 +0x4a
这里的“0xc000078f48, 0x2, 0x4, 0x0, 0x7”是什么意思呢?
如果是传入参数值,为什么和定义的个数对应不上?
日志解析
结论先行
- panic日志中,打印出来的确实是输入参数的值;如果函数有返回值,则返回值也会打印;
- 但实际上是以字长word来打印的(word:操作系统处理信息的基本单位);
- 而每个参数占多少word,又和参数的类型有关(所以我们才会有参数个数对不上的疑惑)。
类型与字长
golang中基础类型如int、char、byte等的字长和C语言一致,不再展开,
下面列举常用的几个:
- 指针占一个word;
- string占两个word (一个指向不可变字符数组的指针,一个string的长度);
- 切片占三个word (一个指向底层数组的指针,一个切片的长度,一个切片的容量)
- 接口占两个word(一个指向实际类型的指针,一个指向数据的指针)
- 更详细的可参考:https://research.swtch.com/godata
堆栈解析
有了上面的知识储备,本文开头提到的panic信息就能解释通了。
func Fun1(slice []string, t *Test, i int)
main.Fun1(0xc000078f48, 0x2, 0x4, 0x0, 0x7)
/Users/leidingyu/workspace/go_proj/src/git.code.oa.com/SNG_EDU_GO_SVR/unittest/main.go:33 +0x4a
第一个参数slice []string,因为切片类型占3个word,所以:
slice := make([]string, 2, 4)
// 该切片的实际值
Pointer: 0xc000078f48
Length: 0x2
Capacity: 0x4// 定义
func Fun1(++slice []string++, t *Test, i int)// 堆栈
main.Fun1(++0x2080c3f50, 0x2, 0x4++, 0x0, 0x7)
第二个参数t *Test,因为指针占1个word,所以:
实际调用
Fun1(slice, nil, 7)// 定义
func Fun1(slice []string, ++t *Test++, i int)// 堆栈
main.Fun1(0x2080c3f50, 0x2, 0x4, ++0x0++, 0x7)
因此从这里也能看出传入的t是nil,这也是panic的所在;
第三个参数i int,因为int占1个word,所以:
//定义 func Fun1(slice []string, t *Test, ++i int++)
//堆栈 main.Fun1(0x2080c3f50, 0x2, 0x4, 0x0, ++0x7++)
函数有返回值
这里增加一个 有两个返回值的函数Fun2:
func Fun2(slice []string, t *Test, i int) (int, error) {
Fun1(slice, t, i) // 此处调用Fun1,用于有无返回值时的对比
return 0, nil
}
再看堆栈信息:
1 main.Fun1(0xc000078f48, 0x2, 0x4, 0x0, 0x7)
/Users/leidingyu/workspace/go_proj/src/git.code.oa.com/SNG_EDU_GO_SVR/unittest/main.go:33 +0x4a
2 main.Fun2(0xc000078f48, 0x2, 0x4, 0x0, 0x7, 0x1056c1d, 0xc000078f88, 0x1004c30)
/Users/leidingyu/workspace/go_proj/src/git.code.oa.com/SNG_EDU_GO_SVR/unittest/main.go:19 +0x53
3 main.main()
/Users/leidingyu/workspace/go_proj/src/git.code.oa.com/SNG_EDU_GO_SVR/unittest/main.go:15 +0x74
由上述第2行可以清晰看出,Fun2有两个返回值(int, error),
堆栈日志中main.Fun2就增加了0x1056c1d, 0xc000078f88, 0x1004c30三个值,其中:
int占一个word,对应0x1056c1d;
error是interface,占两个word,对应0xc000078f88, 0x1004c30;
函数到方法
将上述的Fun2改成如下方式并调用:
// 定义
type M struct {}
func (m *M) Fun2(slice []string, t *Test, i int) (int, error) {
fmt.Printf("m: %p\n", m)
Fun1(slice, t, i)
return 0, nil
}
// 调用
m := new(M)
m.Fun2(slice, nil, 7)
修改前后堆栈日志对比:
// 修改前
main.Fun2(0xc000078f48, 0x2, 0x4, 0x0, 0x7, 0x1056c1d, 0xc000078f88, 0x1004c30)
// 修改后
main.(*M).Fun2(0x1183f88, 0xc000078f48, 0x2, 0x4, 0x0, 0x7, 0x1056c1d, 0xc000078f88, 0x1004c30)
由上可知,两者唯一差别是,func (m *M) Fun2 堆栈的第一个是 m的地址,其他的和 func Func2 一致。
引申问题
参数个数限制
进一步发现堆栈中的Fun2最多只能有10个参数,当有更多时候,会用...省略掉:
main.(*M).Fun2(0x1183f88, 0xc00007cf48, 0x2, 0x4, 0x0, 0x7, 0x0, 0x0, 0x0, 0x0, ...)
/Users/leidingyu/workspace/go_proj/src/git.code.oa.com/SNG_EDU_GO_SVR/unittest/main.go:24 +0xb0
参数packing
前面说了,堆栈参数是以word为单位来打印的,那如果参数不足word长度呢,如bool,char等?还是很有趣的,请看下文:
func main() {
Fun1(true, 40, true, 25)
}
func Fun1(b1 bool, b2 byte, b3 bool, c byte) {
defer func() {
err := recover()
if err != nil {
stackStr := string(debug.Stack())
fmt.Println(stackStr)
}
}()
panic("here")
}
Fun1的堆栈日志:
main.Fun1(0xc019012801)
很明显:b1、b2、b3、c占用了一个word,对应0x19010001,这就是参数packing;
0x19010001每一位都是16进制表示,拆开来看:
19 --> 1*16 + 9*1 = 25
01 --> 1
28 --> 2*16 + 8*1 = 40
01 --> 1
因此可知:堆栈参数中的高位对应着右边的参数,地位对应着左边的参数。
喜欢的话,关注我的公众号哦
本公众号希望从日常工作中的一个小点,深入浅出讲解golang、后台开发的知识点,欢迎一起探讨。
有疑问加站长微信联系(非本文作者)