likes
comments
collection
share

Go语言基础(3)—— nil != nil问题

作者站长头像
站长
· 阅读数 26

Go语言基础之内存模型

先卖个关子

func testInterface() Eatable {
	var result *Student
	if 1 == 2 {
		result = &Student{}
	}
	fmt.Println(result == nil)
	return result
}

func main() {
	fmt.Println(testInterface() == nil)
}

上面这段代码有两个打印输出,猜猜会输出什么

  1. true、true
  2. true、false
  3. false、true
  4. false、false

第一个打印是testInterface函数中的,肯定是true这其实没啥话说,因为if中的赋值操作没办法执行到,result这个指针类型的变量就是一直是初始值,在go中指针类型其实就是uint类型,初始值是0,也就是nil,咱们可以用fmt.Println(uintptr(unsafe.Pointer(result)))试试,也能打印出0的,因此正确答案其实是1或2。那么第二个打印,也就是testInterface函数到底返回的是不是nil呢?按理来说,既然第一个打印说明了result就是个nil,并且第一个打印的下一行代码就是直接将result给return了,那么testInterface就应该返回nil呀。然而,上面这段代码运行起来输出的结果却是答案2

一些改动的尝试

作为一个合格的工程师,线上遇到问题第一时间应该是止损而不是纠结于问题的根本原因,根本原因咱慢慢地来抽丝剥茧。上面的代码导致nil != nil,为了代码能快速地正常运行,那咱就先让testInterface能正常返回预期结果,先尝试改动一下。会不会是testInterface的函数签名中返回值是Eatable和函数体中真正返回的result不是同一种类型导致的?为了验证这个猜想,可以有下面两种修改方式:

  • 把testInterface函数签名的返回值类型改成*Student
func testInterface() *Student {
	var result *Student
	if 1 == 2 {
		result = &Student{}
	}
	fmt.Println(result == nil)
	return result
}
  • 将函数体内的result改成Eatable类型的变量
func testInterface() Eatable {
	var result Eatable
	if 1 == 2 {
		result = &Student{}
	}
	fmt.Println(result == nil)
	return result
}

上面两种修改方式都在最开始的代码中去run一下,发现最终结果都变成了true、true,这说明还真是函数签名的返回值和函数体中真正返回值的类型不一致导致的。但是,why?在Java和c++中就不存在这种问题呀

除此之外还有第三种改法

  • 在第一种改法的基础上,再在main函数中用一个Eatable的变量去指向testInterface函数的返回值,再比较Eatable类型的变量是否等于nil
func testInterface() *Student {
   var result *Student
   if 1 == 2 {
      result = &Student{}
   }
   fmt.Println(result == nil)
   return result
}

func main() {
   var i Eatable
   i = testInterface()
   fmt.Println(i == nil)
}

结果发现这第三种改法的打印输出也是true、false,对于这第三种改法,其实也是函数的返回值与变量类型不匹配导致的。

  • 由此还有第四种改法,在第二种改法的基础上,用一个Eatable的变量去指向testInterface函数的返回值
func testInterface() Eatable {
   var result Eatable
   if 1 == 2 {
      result = &Student{}
   }
   fmt.Println(result == nil)
   return result
}

func main() {
   var i Eatable
   i = testInterface()
   fmt.Println(i == nil)
}

这第四种改法的打印输出和预想的一致,是true、true,这基本上已经证实了是类型匹配的问题

探究问题本质

用反射瞧瞧

和Java一样,Go也是有反射的。Go的反射是使用reflect包,其中reflect.Type表示反射得到的类类型,可以理解成Java中的类的Class。 对于最开始的代码,我们打印一下它的返回值的Type

func testInterface() Eatable {
   var result *Student
   if 1 == 2 {
      result = &Student{}
   }
   fmt.Println(result == nil)
   return result
}

func main() {
   fmt.Println(reflect.TypeOf(testInterface()))
}

最后在main函数中输出的结果为*main.Student,这和testInterface函数中定义的result类型是一致的,可以理解,这说明refelct.TypeOf可以拿到返回的值的具体类型,即使返回的值的类型是个接口,这也与Java中的反射功能实现的效果相同。同时,我们可以断定,main函数中虽然testInterface函数返回的值是*main.Student类型的,但其值一定是nil

几个有意思的等式与不等式

在Java中,我们可以将null强转成任何类型,其实在Go中也可以,那么我们是不是可以比较下testInterface函数的返回值和*Student(nil)是否相等呢?

func main() {
    fmt.Println(testInterface() == (*Student)(nil))
}

结果输出了true,这也验证了我们的猜想。那为啥testInterface() == nil就是false呢,这说明nil != (*Student)(nil)?

咱试试看

func main() {
    fmt.Println((*Student)(nil) == nil)
}

结果居然输出了true,这说明nil == (*Student)(nil)呀,这就有意思了

testInterface() == (*Student)(nil)
(*Student)(nil) == nil
testInterface() != nil

上面三个式子居然同时成立,也就是说A==B,B==C,但A!=C,有点离散数学那味道了吼。

这三个式子,咱一起看确实不太符合现实,那咱就拆开一个一个地看

  • 对于第一个式子,前面用反射看了,testInterface的返回值就是*Student类型的,而且值是0,因此第一个式子是true可以理解
  • 第二个式子,值是nil,那么与nil对比,是true也可以理解
  • 第三个式子,看不懂为啥是不相等 既然testInterface函数签名的返回值是Eatable接口类型的,那么和Eatable(nil)对比一下呢?因此就有了第四个式子
testInterface() == Eatable(nil)

打印输出结果是false,那当然了testInterface返回的具体值是(*Student)(nil),(*Student)(nil)!=Eatable(nil)也是能够理解的,咱验证一下,第五个式子就是

(*Student)(nil)!=Eatable(nil)

打印输出结果是ture,猜想正确。

接下来,咱突然有了一个大胆的猜想,不会testInterface()!=nil就是testInterface()!=Eatable(nil)吧? 要是真的话,而由前面反射得到testInterface得到的Type是*Student,这就能说得通testInterface()==nil是false了。

nil是有类型的

经过前面的分析,我们发现nil在各种场景中会有不同的表现,说到底,其实是因为nil在不同的场景下是带了类型的,不同于c/c++的NULL是0,go中的每种类型的nil都有不同的含义。

func main() {
   var i1 Eatable = nil
   var i2 *Student = nil
   var i3 *int = nil
   var i4 interface{} = nil
   fmt.Println(i1, i2, i3, i4)
   fmt.Printf("%p, %p, %p, %p\n", i1, i2, i3, i4)
   fmt.Println(reflect.TypeOf(i1), reflect.TypeOf(i2), reflect.TypeOf(i3),  reflect.TypeOf(i4))
}

上面的代码的输出为

<nil> <nil> <nil> <nil>
%!p(<nil>), 0x0, 0x0, %!p(<nil>)
<nil> *main.Student *int <nil>

特别是第三行打印输出,说明接口类型的变量如果是nil的话,就保存不了类型 如果接口类型的变量我们给赋值成对应实现了接口的struct的指针nil呢?

func main() {
   var i1 Eatable = (*Student)(nil)
   var i2 *Student = nil
   var i3 *int = nil
   var i4 interface{} = (*Student)(nil)
   fmt.Println(i1, i2, i3, i4)
   fmt.Printf("%p, %p, %p, %p\n", i1, i2, i3, i4)
   fmt.Println(reflect.TypeOf(i1), reflect.TypeOf(i2), reflect.TypeOf(i3), reflect.TypeOf(i4))

输出结果变成了

<nil> <nil> <nil> <nil>
0x0, 0x0, 0x0, 0x0
*main.Student *main.Student *int *main.Student

这说明接口类型的变量其实是可以保存实际值的类型的,go中用iface表示Eatable这种有函数的接口,而用eface表示interface{}这种没有函数的接口。iface和eface都可以在go源码的runtime2.go中找到定义。

type iface struct {
   tab  *itab
   data unsafe.Pointer
}

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

go相对于Java来说,还有一个特点,那就是拥有unsafe包,可以执行一些强转,类似于c/c++能够做到的指针强转,因此我们可以用unsafe包将变量的指针强转成iface或eface

e := testInterface()
face := (*iface)(unsafe.Pointer(&))
fmt.Println(*face)

上面的代码会将testInterface的返回值强转成iface类型,如果testInterface的实现是最开始的代码,会打印出{0x4b89f8 <nil>},前面一个值是itab类型的指针,不为nil说明其是有值的。这里需要说明一下iface和eface这两个strct的内部字段。其中的data都显示这个接口变量指向的数据的地址,而tab和_type字段都表示接口变量指向的数据的具体类型,因此0x4b89f8这个itab指针其实就表示*Student这种类型,也因此带有类型的接口变量不为nil,也就是iface或eface中,即使第二个字段为nil(也就是没有真实数据)但第一个字段不为nil,这个接口变量也不为nil。

结论

nil != nil的问题究其根本,其实是因为go的接口的内存模型导致的。在# Go语言基础(2)——数据、函数与接口一文中提到过,go中struct实现interface只需要实现其定义的所有函数,而不用显式地声明struct实现了interface。因此interface类型的变量就必须保存实际指向的struct的类型和内存地址,可以把内存地址理解struct实例在内存中的起始地址,而类型可以理解成在内存中的长度。只有同时持有实例的起始地址和内容长度,才能保证访问内存的安全性。

避坑方案

  1. 函数签名中的返回值与实现中的返回值类型相同 这样主要是可以让返回之后的值与nil比较时可以实现同类型比较(*Student)(nil) == (*Student)(nil) 但如果函数签名是指针类型的,而函数调用却用一个接口类型的变量接收返回值,再拿这个接口类型的变量去与nil比较,还是会出现nil != nil问题,因此此方案还是有坑
  2. 在1的基础上,尽量使用短声明模式,e := testInterface(),这样可以避免使用方案1之后的坑
  3. 在函数实现时,尽量在返回nill的直接return nil,而不是将result赋值成nil后最终返回一个result,但其实这种方案还是可能有1中的坑。