panic日志
1 | [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x512f5d] |
可以看到,是json encode string的时候发生的panic,这种情况比较少见。
重点是这一行日志:
encoding/json.(*encodeState).string(0xc004eba100, 0x0, 0x7, 0x1)
string()
函数的入参有0x0和0x7
实际上这里的0x0是指字符串变量指向内容的指针为0,表示长度的变量为0x7
原理
string
类型底层实现是一个指向内容的指针和一个表示长度的int
1 | // StringHeader is the runtime representation of a string. |
对字符串的赋值,分为两部分,所以在go里对字符串进行并发读写的话,有可能会导致指向的内容和长度不匹配,导致一些奇怪的问题。
比如两个协程同时并发读写同一个字符串变量,一个赋值为”123”,另一个赋值为”123456”,该字符串有可能为”123”,但长度是6。
但是就算并发读写,Data指针指向的一直是正常地址,只是长度不一定匹配,又怎么会导致nil pointer的呢?
其实,在go里,有一种特殊优化,如果你把字符串赋值为””,即空字符串的话,Data指针是0,也即空指针。
我们来验证一下:
1 | package main |
运行后发生panic
1 | $ go run main.go |
看下汇编代码,略有精简
go tool compile -S -N -l main.go
1 | ... |
MOVQ $0, 72(CX)
表示把立即数0存入CX地址+72,即把长度0存入Len
MOVQ $0, 64(CX)
表示把立即数0存入CX地址+64,即指向字符串内容的指针值为0,也就是nil pointer
1 | ... |
MOVQ $7, 72(CX)
表示把立即数7存入CX地址+72,即把长度0存入Len
1 | LEAQ go.string."default"(SB), AX |
表示把指向”default”的地址存入AX,再把AX存入64(CX),即把指向字符串内容的指针赋值为”default”的地址
所以在json encode这个字符串的时候,可能会发生字符串有长度,但指向内容的指针是空指针,
可以通过len(str)>0这样的判断,但真正去取值的时候就发生了nil pointer panic。
更进一步
所以可见,如果某个字符串变量存储了很多的内容,可以通过赋值为空字符串进行释放。
那么问题又来了,如果按上面的分析,赋值为空字符串,指针就会为空,那为什么如下代码没有发生panic
1 | a := "" |
其实编译器会判断到如果这个空字符串在后续是有取指针操作的话,加上如下汇编代码:
1 | 0x003c 00060 (main.go:9) MOVQ "".&p+24(SP), DI |
以保证可以正常取到指针