golang panic堆栈日志解读

竹一先生_阳明学子 · · 842 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

问题由来

以前出现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

因此可知:堆栈参数中的高位对应着右边的参数,地位对应着左边的参数。

喜欢的话,关注我的公众号哦

image.png

本公众号希望从日常工作中的一个小点,深入浅出讲解golang、后台开发的知识点,欢迎一起探讨。


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:竹一先生_阳明学子

查看原文:golang panic堆栈日志解读

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

842 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传