问题由来
以前出现panic问题,总是习惯通过日志中给出的代码行,去“猜测”是哪个变量出了问题,
如果推断不出来,就多加入一些日志,重现panic,再继续定位。
昨天又遇到了panic的问题,看到屏幕上打印了很多堆栈日志,转念一想:
如果现网出现了panic但日志信息不够怎么办,总不能先加日志等下次重现后再定位吧?
然后尝试仔细阅读堆栈日志时,却多出了一些疑惑:
日志解析
结论先行
panic日志中,打印出来的确实是输入参数的值;如果函数有返回值,则返回值也会打印;
但实际上是以字长word来打印的(word:操作系统处理信息的基本单位);
而每个参数占多少word,又和参数的类型有关(所以我们才会有参数个数对不上的疑惑)。
类型与字长
golang中基础类型如int、char、byte等的字长和C语言一致,不再展开,
下面列举常用的几个:
指针占一个word;
string占两个word (一个指向不可变字符数组的指针,一个string的长度);
切片占三个word (一个指向底层数组的指针,一个切片的长度,一个切片的容量)
接口占两个word(一个指向实际类型的指针,一个指向数据的指针)
更详细的可参考:https://research.swtch.com/godata
堆栈解析
有了上面的知识储备,本文开头提到的panic信息就能解释通了。
第一个参数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:
再看堆栈信息:
由上述第2行可以清晰看出,Fun2有两个返回值(int, error),
堆栈日志中main.Fun2就增加了0x1056c1d, 0xc000078f88, 0x1004c30三个值,其中:
int占一个word,对应0x1056c1d;
error是interface,占两个word,对应0xc000078f88, 0x1004c30;
函数到方法
将上述的Fun2改成如下方式并调用:
修改前后堆栈日志对比:
由上可知,两者唯一差别是,func (m *M) Fun2 堆栈的第一个是 m的地址,其他的和 func Func2 一致。
引申问题
参数个数限制
进一步发现堆栈中的Fun2最多只能有10个参数,当有更多时候,会用...省略掉:
参数packing
前面说了,堆栈参数是以word为单位来打印的,那如果参数不足word长度呢,如bool,char等?还是很有趣的,请看下文:
Fun1的堆栈日志:
很明显:b1、b2、b3、c占用了一个word,对应0x19010001,这就是参数packing;
因此可知:堆栈参数中的高位对应着右边的参数,地位对应着左边的参数。
喜欢的话,关注我的公众号哦
领取专属 10元无门槛券
私享最新 技术干货