go 空指针探究

背景

最近发现在空指针上有一些问题,在开发过程中 nil 的判断有时候没有起作用。导致了 panic。

研究

go 里面对象从反射看分为 type 和 value 即动态类型和值。
在 go 中 nil 也是有类型的,以下是输出 nil 的 type 和 value

1
<nil> <invalid reflect.Value>

go 在一般定义对象时候会赋予初始值,如果我们没有设置初始化的值。基础类型不说,指针会是 nil,接口也是 nil,struct 就是创建了一个空的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type TestPointer interface {
Print() error
}

type TestPoint struct {
}

func (t *TestPoint) Print() error {
fmt.Println("", reflect.TypeOf(t), reflect.ValueOf(t))
return nil
}

var tPoint *TestPoint
var tInterface TestPointer
var tPointS TestPoint

关键来了,指针和接口的 nil 实际上不是一回事。指针赋予的初始值 nil,type 是指针类型,value 是不存在。接口的初始值就是 nil,type 是 nil,value 是不存在。

1
2
assert.True(t, tPoint == nil)
assert.True(t, tInterface == nil)

这断言运行起来没问题,是不是看上去很正常。

1
2
3
4
5
6
tfunc := func(t TestPointer) bool {
fmt.Println("", reflect.TypeOf(t), reflect.ValueOf(t))
return t == nil
}
assert.False(t, tfunc(tPoint))
assert.True(t, tfunc(tInterface))

这断言 ok,细心看就会发现 tfunc(tPoint) 返回值是 false 了。
这里面有2个问题。

  1. 为什么 tfunc(tPoint) 是 false。
  2. 为什么同样都是 nil,传递到方法里面判断就会产生不一样的后果。
    这是不是意味着 nil 判断其实是不准确的。通过打印 reflect.TypeOf(t), reflect.ValueOf(t) ,这2个值是不一样的(但结果在方法内还是方法外是一样的)。
    1
    2
    *user.TestPoint <nil>
    <nil> <invalid reflect.Value>
    这种情况下不影响对 nil 的判断。但是将 tPoint 作为参数类型是 TestPointer 传入则判断出错,我们用 print() 打印一下。
    1
    2
    0x0
    (0x0,0x0)
    ,在方法内打印结果是
    1
    2
    (0x1225ba0,0x0)
    (0x0,0x0)
    这其实表明了一点是,在 go 中,将具体类型作为接口类型传入时,会产生指针,指针会指向具体的类型。从这里大概解释了问题。

底层

interface 使用 iface 和 eface 数据结构来表示。这个从上面可知,在接口参数 传入 struct 时,是(0x1225ba0,0x0),说明 tab 不是 nil,data 是 nil。而 == nil 判断需要 tab 和 data 都是 nil。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type iface struct {
tab *itab
data unsafe.Pointer
}

type itab struct {
inter *interfacetype // 8 字节
_type *_type // 8字节
hash uint32 // 4字节,与 _type 里 hash 相同
_ [4]byte // 4字节
fun [1]uintptr // 8字节,函数多了往后偏移,variable sized. fun[0]==0 means _type does not implement inter.
}

type eface struct {
_type *_type
data unsafe.Pointer
}

结论

  1. 如果方法传参是定义好的 interface,那么在方法内部判 nil 的时候需要特别注意,如果外界传入的是实现了 interface 的 struct, 判 nil 会出现问题,可以将其转为特定类型或者用反射来判断。
  2. 如何避免出现这个问题。在别的语言比如 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 只能不断加深理解,不断挖坑填坑。