go 空指针探究
背景
最近发现在空指针上有一些问题,在开发过程中 nil 的判断有时候没有起作用。导致了 panic。
研究
go 里面对象从反射看分为 type 和 value 即动态类型和值。
在 go 中 nil 也是有类型的,以下是输出 nil 的 type 和 value
1 | <nil> <invalid reflect.Value> |
go 在一般定义对象时候会赋予初始值,如果我们没有设置初始化的值。基础类型不说,指针会是 nil,接口也是 nil,struct 就是创建了一个空的对象。
1 | type TestPointer interface { |
关键来了,指针和接口的 nil 实际上不是一回事。指针赋予的初始值 nil,type 是指针类型,value 是不存在。接口的初始值就是 nil,type 是 nil,value 是不存在。
1 | assert.True(t, tPoint == nil) |
这断言运行起来没问题,是不是看上去很正常。
1 | tfunc := func(t TestPointer) bool { |
这断言 ok,细心看就会发现 tfunc(tPoint) 返回值是 false 了。
这里面有2个问题。
- 为什么 tfunc(tPoint) 是 false。
- 为什么同样都是 nil,传递到方法里面判断就会产生不一样的后果。
这是不是意味着 nil 判断其实是不准确的。通过打印 reflect.TypeOf(t), reflect.ValueOf(t) ,这2个值是不一样的(但结果在方法内还是方法外是一样的)。这种情况下不影响对 nil 的判断。但是将 tPoint 作为参数类型是 TestPointer 传入则判断出错,我们用 print() 打印一下。1
2*user.TestPoint <nil>
<nil> <invalid reflect.Value>,在方法内打印结果是1
20x0
(0x0,0x0)这其实表明了一点是,在 go 中,将具体类型作为接口类型传入时,会产生指针,指针会指向具体的类型。从这里大概解释了问题。1
2(0x1225ba0,0x0)
(0x0,0x0)
底层
interface 使用 iface 和 eface 数据结构来表示。这个从上面可知,在接口参数 传入 struct 时,是(0x1225ba0,0x0),说明 tab 不是 nil,data 是 nil。而 == nil 判断需要 tab 和 data 都是 nil。
1 | type iface struct { |
结论
- 如果方法传参是定义好的 interface,那么在方法内部判 nil 的时候需要特别注意,如果外界传入的是实现了 interface 的 struct, 判 nil 会出现问题,可以将其转为特定类型或者用反射来判断。
- 如何避免出现这个问题。在别的语言比如 java 等面向对象的,没有这个问题。go 中,因为采用了 struct 自动转 interface ,这导致和 nil 的定义相冲突,产生了此类问题。nil 的定义 go 很清晰
var nil Type nil is a predeclared identifier representing the zero value for a pointer, channel, func, interface, map, or slice type
。interface 的解析也很清楚。但是2者结合就容易出现理解出问题,想消除 bug 只能不断加深理解,不断挖坑填坑。