问题由来

以前出现panic问题,我们会习惯通过日志给出的代码行,在对比数据分析问题,

如果推断不出来,我们会多加日志,重新panic,再继续定位。

如果打印了很多堆栈日志,但是日志信息不够怎么办,我们总不能加日志等下次重现后再定位吧?

这样定位bug的工作效率特别低,所以我们可以仔细阅读堆栈日志,结合打印的日志快速定位bug。

在阅读堆栈日志时,可能我们会疑惑:

为什么定义是这样:
func TestStack(slice []string, str string, i int)
 
但堆栈日志显示:
main.TestStack(0xc00010ff38, 0x2, 0x4, 0xabcac1, 0x5, 0xa)
         E:/go-workstation/golangsutdy/stack-tace/main.go:20 +0x65这里的0xc00010ff38, 0x2, 0x4, 0xabcac1, 0x5, 0xa是什么意思?
如果是传入参数值,为什么和定义的个数对不上?

日志分析

分析必备

1. panic日志中,打印出来的确实是输入参数的值,如果函数有返回值,返回值也会打印。

2.但实际上是以字长word去打印的(word:操作系统处理信息的基本单位)

3.而每个参数占多少word,又和参数类型有关。

类型与字长

golang中基本类型如int、char、byte等的字长和C语言一致,不再展开,

下面列举常用的几个:

1.指针占一个word;
2.string占两个word(一个指向不可变字符数组指针,一个string的长度);
3.切片占三个word(一个指向底层数组指针,一个切片长度,一个切片容量)
4,接口占两个word(一个指向实际类型指针,一个指向数据的指针)

堆栈分析

现在我们就解释panic信息

func TestStack(slice []string, str string, i int)
main.TestStack(0xc00010ff38, 0x2, 0x4, 0xabcac1, 0x5, 0xa)
         E:/go-workstation/golangsutdy/stack-tace/main.go:20 +0x65

第一个参数是[]string,因为切片类型占3个word,所以

slice := make([]string,2,4)

切片实际值:

Pointer: 0xc00010ff38
Length:  0x2
Capacity: 0x4

第二个参数是str string, 因为string占两个word,所以

Pointer: 0xabcac1
Length:  0x5

第三个参数是i int,因为int占一个work,所以

0xa   i=10

函数有返回值

这里增加一个有返回值的函数Fun2:

func TestSt2(slice []string, str string, i int) ( *int,  error)

再看TestSt2的日志信息:

main.TestSt2(0xc00010ff38, 0x2, 0x4, 0x66dac1, 0x5, 0xa, 0x1056c1d, 0xc000078f88, 0x1004c30)
  E:/go-workstation/golangsutdy/stack-tace/main.go:32 +0x60

 对比 TestStack的日志信息发现,TestSt2增加了0x1056c1d, 0xc000078f88, 0x1004c30三个值,其中:

*int是指针占一个word,对应是0x1056c1d,

error是interface,占两个word,对应0xc000078f88, 0x1004c30

函数到方法

上述的TestStack改成如下方式并调用

type trace struct{}

func main() {
   slice := make([]string, 2, 4)
   var t trace
   t.TestStack(slice, "hello", 10)
}

func (t *trace) TestStack(slice []string, str string, i int) {
   fmt.Printf("Receiver Address: %p\n", t)
   panic("Want stack trace")
}

修改前后堆栈日志对比

//修改前
main.TestStack(0xc00010ff38, 0x2, 0x4, 0xabcac1, 0x5, 0xa)
         E:/go-workstation/golangsutdy/stack-tace/main.go:20 +0x65//修改后
main.(*trace).TestStack(0x83cde0, 0xc00010ff38, 0x2, 0x4, 0x75ea41, 0x5, 0xa)
         E:/go-workstation/golangsutdy/stack-tace/main.go:15 +0xaf

 

由上可知,两者唯一差别,func (t *trace) TestStack堆栈的第一个是t的地址,其他和func TestStack一致。

参数packing

前面说了,堆栈参数是以word为单位打印的,那如果参数不够word长度呢,如bool,char等?还是请看下文:

func main() {
   Example(true, false, true, 25)
}
func Example(b1, b2, b3 bool, i uint8) {
   defer func() {
      err := recover()
      if err != nil {
         stackStr := string(debug.Stack())
         fmt.Println(stackStr)
      }
   }()
   panic("Want stack trace")
}

堆栈日志:

main.Example(0xc019010001)
         E:/go-workstation/golangsutdy/stack-tace/main.go:19 +0x65

很明显b1,b2,b3,i占了一个word,对应0xc019010001,这个就是参数packing

0xc019010001每一位都是16进制,拆开来看

19  -->  1*16+9*1 = 25
01 --> 1
00 --> 0
01 --> 1