panic日志 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x512f5d] goroutine 48438156 [running]: encoding/json.(*encodeState).marshal.func1(0xc009decb40) /go/src/encoding/json/encode.go:326 +0x9a panic(0xd41f60, 0x187d170) /go/src/runtime/panic.go:967 +0x15d encoding/json.(*encodeState).string(0xc004eba100, 0x0, 0x7, 0x1) /go/src/encoding/json/encode.go:1003 +0x5d encoding/json.stringEncoder(0xc004eba100, 0xce54e0, 0xc008bfeea0, 0x198, 0x100) /go/src/encoding/json/encode.go:644 +0x358 encoding/json.structEncoder.encode(0xc00451c000, 0x13, 0x21, 0xc0058d99e0, 0xc004eba100, 0xe29520, 0xc008bfeea0, 0x199, 0xd30100) /go/src/encoding/json/encode.go:758 +0x2bb encoding/json.ptrEncoder.encode(0xc0058d9a70, 0xc004eba100, 0xd26880, 0xc008bfeea0, 0x16, 0xd20100) /go/src/encoding/json/encode.go:914 +0x116 encoding/json.(*encodeState).reflectValue(0xc004eba100, 0xd26880, 0xc008bfeea0, 0x16, 0xc009de0100) /go/src/encoding/json/encode.go:358 +0x82 encoding/json.(*encodeState).marshal(0xc004eba100, 0xd26880, 0xc008bfeea0, 0x450100, 0x0, 0x0) /go/src/encoding/json/encode.go:330 +0xf0 encoding/json.Marshal(0xd26880, 0xc008bfeea0, 0x18cf180, 0x8, 0xc0029067a8, 0xc002038c40, 0x15) /go/src/encoding/json/encode.go:161 +0x52 ...
可以看到,是json encode string的时候发生的panic,这种情况比较少见。
重点是这一行日志:
encoding/json.(*encodeState).string(0xc004eba100, 0x0, 0x7, 0x1)
string()
函数的入参有0x0和0x7
实际上这里的0x0是指字符串变量指向内容的指针为0,表示长度的变量为0x7
原理 string
类型底层实现是一个指向内容的指针和一个表示长度的int
1 2 3 4 5 6 7 8 9 10 type StringHeader struct { Data uintptr Len int }
对字符串的赋值,分为两部分,所以在go里对字符串进行并发读写的话,有可能会导致指向的内容和长度不匹配,导致一些奇怪的问题。
比如两个协程同时并发读写同一个字符串变量,一个赋值为”123”,另一个赋值为”123456”,该字符串有可能为”123”,但长度是6。
但是就算并发读写,Data指针指向的一直是正常地址,只是长度不一定匹配,又怎么会导致nil pointer的呢?
其实,在go里,有一种特殊优化,如果你把字符串赋值为””,即空字符串的话,Data指针是0,也即空指针。
我们来验证一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 package mainimport ( "fmt" "time" ) type AudioClips struct { Param *TableStruct } type TableStruct struct { RequestId string Type string Organization string BtId string AppId string TokenId string Channel string } func f (org, serviceId, appId, channel string ) { curTime := time.Now().Format(layout) fmt.Sprintf("%s@%s@%s@%s@%s@%s" , curTime, org, serviceId, appId, channel, "length" ) } var layout string func main () { a := AudioClips{ Param: &TableStruct{ AppId: "" , Organization: "org" , Channel: "channel" , }, } go func () { for { f(a.Param.Organization, "serviceId" , a.Param.AppId, a.Param.Channel) } }() for { go func () {a.Param.AppId = "" }() time.Sleep(10 *time.Nanosecond) go func () {a.Param.AppId = "default" }() time.Sleep(10 *time.Nanosecond) } }
运行后发生panic
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 $ go run main.go panic: runtime error: invalid memory address or nil pointer dereference [signal 0xc0000005 code=0x0 addr=0x0 pc=0xe570a2] goroutine 34 [running]: fmt.(*buffer).writeString(...) C:/Program Files/Go/src/fmt/print.go:82 fmt.(*fmt).padString(0xc0000202b0, 0x0, 0x7) C:/Program Files/Go/src/fmt/format.go:110 +0x98 fmt.(*fmt).fmtS(0xc0000202b0, 0x0, 0x7) C:/Program Files/Go/src/fmt/format.go:359 +0x68 fmt.(*pp).fmtString(0xc000020270, 0x0, 0x7, 0x21700000073) C:/Program Files/Go/src/fmt/print.go:446 +0x1d2 fmt.(*pp).printArg(0xc000020270, 0xead6c0, 0xc0003de2b0, 0x73) C:/Program Files/Go/src/fmt/print.go:694 +0x875 fmt.(*pp).doPrintf(0xc000020270, 0xec9c4d, 0x11, 0xc000097f18, 0x6, 0x6) C:/Program Files/Go/src/fmt/print.go:1026 +0x168 fmt.Sprintf(0xec9c4d, 0x11, 0xc000097f18, 0x6, 0x6, 0x0, 0x0) C:/Program Files/Go/src/fmt/print.go:219 +0x6d main.f(0xec799a, 0x3, 0xec8574, 0x9, 0x0, 0x7, 0xec7fd9, 0x7) C:/Projects/test/11/main.go:43 +0x1de main.main.func1(0xc000194000) C:/Projects/test/11/main.go:59 +0x8a created by main.main C:/Projects/test/11/main.go:57 +0x91 exit status 2
看下汇编代码,略有精简
go tool compile -S -N -l main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 ... "".main.func2 STEXT size=92 args=0x8 locals=0x8 0x0000 00000 (main.go:47) TEXT "".main.func2(SB), ABIInternal, $8-8 0x0000 00000 (main.go:47) MOVQ (TLS), CX 0x0009 00009 (main.go:47) CMPQ SP, 16(CX) 0x000d 00013 (main.go:47) PCDATA $0, $-2 0x000d 00013 (main.go:47) JLS 85 0x000f 00015 (main.go:47) PCDATA $0, $-1 0x000f 00015 (main.go:47) SUBQ $8, SP 0x0013 00019 (main.go:47) MOVQ BP, (SP) 0x0017 00023 (main.go:47) LEAQ (SP), BP 0x001b 00027 (main.go:47) MOVQ "".a+16(SP), CX 0x0020 00032 (main.go:47) TESTB AL, (CX) 0x0022 00034 (main.go:47) MOVQ $0, 72(CX) 0x002a 00042 (main.go:47) LEAQ 64(CX), DI 0x002e 00046 (main.go:47) CMPL runtime.writeBarrier(SB), $0 0x0035 00053 (main.go:47) JEQ 57 0x0037 00055 (main.go:47) JMP 76 0x0039 00057 (main.go:47) MOVQ $0, 64(CX) 0x0041 00065 (main.go:47) JMP 67 0x0043 00067 (main.go:47) PCDATA $0, $-1 0x0043 00067 (main.go:47) PCDATA $1, $-1 0x0043 00067 (main.go:47) MOVQ (SP), BP 0x0047 00071 (main.go:47) ADDQ $8, SP 0x004b 00075 (main.go:47) RET ...
MOVQ $0, 72(CX)
表示把立即数0存入CX地址+72,即把长度0存入Len
MOVQ $0, 64(CX)
表示把立即数0存入CX地址+64,即指向字符串内容的指针值为0,也就是nil pointer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 ... "".main.func3 STEXT size=100 args=0x8 locals=0x8 0x0000 00000 (main.go:49) TEXT "".main.func3(SB), ABIInternal, $8-8 0x0000 00000 (main.go:49) MOVQ (TLS), CX 0x0009 00009 (main.go:49) CMPQ SP, 16(CX) 0x000d 00013 (main.go:49) PCDATA $0, $-2 0x000d 00013 (main.go:49) JLS 93 0x000f 00015 (main.go:49) PCDATA $0, $-1 0x000f 00015 (main.go:49) SUBQ $8, SP 0x0013 00019 (main.go:49) MOVQ BP, (SP) 0x0017 00023 (main.go:49) LEAQ (SP), BP 0x001b 00027 (main.go:49) MOVQ "".a+16(SP), CX 0x0020 00032 (main.go:49) TESTB AL, (CX) 0x0022 00034 (main.go:49) MOVQ $7, 72(CX) 0x002a 00042 (main.go:49) LEAQ 64(CX), DI 0x002e 00046 (main.go:49) PCDATA $0, $-2 0x002e 00046 (main.go:49) PCDATA $1, $-2 0x002e 00046 (main.go:49) CMPL runtime.writeBarrier(SB), $0 0x0035 00053 (main.go:49) JEQ 57 0x0037 00055 (main.go:49) JMP 79 0x0039 00057 (main.go:49) LEAQ go.string."default"(SB), AX 0x0040 00064 (main.go:49) MOVQ AX, 64(CX) 0x0044 00068 (main.go:49) JMP 70 0x0046 00070 (main.go:49) PCDATA $0, $-1 0x0046 00070 (main.go:49) PCDATA $1, $-1 0x0046 00070 (main.go:49) MOVQ (SP), BP 0x004a 00074 (main.go:49) ADDQ $8, SP 0x004e 00078 (main.go:49) RET ...
MOVQ $7, 72(CX)
表示把立即数7存入CX地址+72,即把长度0存入Len
1 2 LEAQ go.string."default"(SB), AX MOVQ AX, 64(CX)
表示把指向”default”的地址存入AX,再把AX存入64(CX),即把指向字符串内容的指针赋值为”default”的地址
所以在json encode这个字符串的时候,可能会发生字符串有长度,但指向内容的指针是空指针,
可以通过len(str)>0这样的判断,但真正去取值的时候就发生了nil pointer panic。
更进一步 所以可见,如果某个字符串变量存储了很多的内容,可以通过赋值为空字符串进行释放。
那么问题又来了,如果按上面的分析,赋值为空字符串,指针就会为空,那为什么如下代码没有发生panic
其实编译器会判断到如果这个空字符串在后续是有取指针操作的话,加上如下汇编代码:
1 2 3 4 5 6 7 0x003c 00060 (main.go:9) MOVQ "".&p+24(SP), DI 0x0041 00065 (main.go:9) MOVQ "".&a+16(SP), AX 0x0046 00070 (main.go:9) CMPL runtime.writeBarrier(SB), $0 0x004d 00077 (main.go:9) JEQ 81 0x004f 00079 (main.go:9) JMP 95 0x0051 00081 (main.go:9) MOVQ AX, (DI) 0x0054 00084 (main.go:9) JMP 86
以保证可以正常取到指针