Go:什么时候nil != nil

最近几年一直在用Go做开发工作,在工作中遇到不少坑,后面找时间挨个来写,今天我们一起来理清楚Go在哪些情况下我们使用 == 操作符号对比时候 nil != nil,以及我们如何避免在代码中遇到这些问题。

我们先定义两个不同类型的变量,每个变量都赋值为nil。

var a *int = nil

var b interface{} = nil

想象一下下面的代码会输出什么结果:

fmt.Println(“a == nil”, a == nil)

fmt.Println(“b == nil”, b == nil)

fmt.Println(“a == b:”, a == b)

运行结果:

a == nil: true

b == nil: true

a == b: false

我们再来看一个类似的例子,我们将b初始化为a

var a *int = nil

var b interface{} = a

fmt.Println(“a == nil:”, a == nil)

fmt.Println(“b == nil:”, b == nil)

fmt.Println(“a == b:”, a == b)

我们再次运行上面代码后,输出的结果如下

a == nil: true

b == nil: false

a == b: true

这是怎么回事?

这个问题不是Go语言bug,这只是一个特定的规则,我们理解这个规则后就能明白经常看到开源代码中写的下面这种代码是为什么了。

if nil == a {

b = nil

}

这里对b赋值之前对a进行了检测并且直接赋值了 nil。

我们需要搞清楚的第一件事,Go中的所有指针都有两个值,一个是类型(表明指针的类型),一个是值(指向具体值).也就是说每个指针都需要有一个类型来表明指针属于什么类型,所以我们不能给一个指针类型赋值为nil。例如下面这个代码就不能被编译

a := nil

为了让这行代码被编译,必须要给这个指针赋值一个类型,然后对它的值赋值为nil,就像这样:

var a *int = nil

var b interface{} = nil

现在这些变量都有了类型,就可以使用 fmt.Printf 函数打印出它们的类型了。

var a *int = nil

var b interface{} = nil

fmt.Printf(“a=(%T, …)\n”, a)

fmt.Printf(“b=(%T, …)\n”, b)

注: %T 格式化符仅仅打印出变量的类型

运行结果:

a=(*int, …)

b=(<nil>, …)

上面的结果显示,当a被赋值为nil时候,它的类型被赋值为 *int;

b是一个interface{}(空接口),但是它被赋值nil时候它的类型却被指定为 <nil>。

这是怎么回事?

我们使用空接口主要是为了符合任何类型的实现。<nil>其实也是一个类型,它符合一个空接口的标准。

好了,我们知道所有的指针都是由 (type, value)这样的组合构成,我们看到了给变量硬编码 nil时候会出现的情况。下面我们来给这些类型赋值,看会有什么变化。

var a *int = nil

var b interface{} = a

fmt.Printf(“a=(%T, …)\n”, a)

fmt.Printf(“b=(%T, …)\n”, b)

运行结果:

a=(*int, …)

b=(*int, …)

运行程序之后你会发现你会得到一个新的结果,b有了一个新的类型。

在我们之前硬编码赋值时候,b是一个<nil>类型的指针。我们来看看使用a给b赋值时候,发生了什么。

比较a和b的值

既然我们了解这些类型被确定,让我们看看会发生什么,当我们在代码中检查a和b相等情况。我们开始对a和b都被分配给硬编码nil。之后,我们将会看到类似的片段,a赋值给b。

var a *int = nil

var b interface{} = nil

fmt.Printf(“a=(%T, %v)\n”, a, a)

fmt.Printf(“b=(%T, %v)\n”, b, b)

fmt.Println()

fmt.Println(“a == nil:”, a == nil)

fmt.Println(“b == nil:”, b == nil)

fmt.Println(“a == b:”, a == b)

运行结果:

a=(*int, <nil>)

b=(<nil>, <nil>)

a == nil: true

b == nil: true

a == b: false

这里很奇怪的是 a和b是不相等的,这是一个很奇怪的状况,看起来像是 a == nil 且 b == nil,所以应该是 a == b,但是实际输出结果并不是这样。

实际上我们写的 a == nil 并不是真正的在比较值,我们实际上是在比较(type,value) 这个结构。我们并不是仅仅比较存储在a中的值.

下面模拟显示一下实际比较的值:

a == nil: (*int, <nil>) == (*int*, <nil>)

b == nil: (<nil>, <nil>) == (<nil>, <nil>)

a == b: (*int, <nil>) == (<nil>, <nil>)

当我们看到上面这个就明白了,a和b实际上是不等的,因为它们的类型并不一样,但是在代码中这里并不会显示展示出来,所以我们会误解它们应该是相等的。

但是如果你这样写代码

if nil == a && nil == b {

//表达式为true

}

下面我们看一下将a赋值给b,会发生什么

var a *int = nil

var b interface{} = a // <- the change

fmt.Printf(“a=(%T, %v)\n”, a, a)

fmt.Printf(“b=(%T, %v)\n”, b, b)

fmt.Println()

fmt.Println(“a == nil:”, a == nil)

fmt.Println(“b == nil:”, b == nil)

fmt.Println(“a == b:”, a == b)

运行结果:

a=(*int, <nil>)

b=(*int, <nil>)

a == nil: true

b == nil: false

a == b: true

现在的结果又有问题了,b == nil 返回 false

当我们运行 b == nil的时候,编译器需要确定给nil一个什么类型,实际上编译器给出了<nil,nil>。但是实际上这个时候b被赋值为 <*int, nil>。明显编译器就不会认为他们是相等的了。

这个地方实际上会造成一些混淆,我们以为编译器会处理这个问题,但是实际上编译器并不能处理这个问题,因为interface{}的类型在运行的过程中随时在发生变化。

比如下面的程序

var a *int = nil

var b interface{} = a

var c *string = nil

fmt.Printf(“b=(%T, %v)\n”, b, b)

fmt.Println(“b == nil:”, b == nil)

b = c

fmt.Printf(“b=(%T, %v)\n”, b, b)

fmt.Println(“b == nil:”, b == nil)

b = nil

fmt.Printf(“b=(%T, %v)\n”, b, b)

fmt.Println(“b == nil:”, b == nil)

运行结果:

b=(*int, <nil>)

b == nil: false

b=(*string, <nil>)

b == nil: false

b=(<nil>, <nil>)

b == nil: true

从这个结果我们就可以看出来,编译器在编译的时候并不能确定b的类型,只能在运行期的时候确定b的具体类型,所以编译器并不能处理这个问题。

我们来看看如何强制让编译器将 nil放进正确类型里,实际上这并不是唯一的情况下让编译器使用这样的类型决定。比如,当你给变量分配一个硬编码的数字,编译器应该使用哪种类型将基于程序的上下文来决定。当声明的变量(如var int = 12),但这也会发生在当我们经过一个硬编码值函数或当我们给变量分配一个数字。所有这些情况如下所示。

程序如下:

package main

import “fmt”

func main() {

var a int = 12

var b float64 = 12

var c interface{} = a

d := 12 // will be an int

fmt.Printf(“a=(%T,%v)\n”, a, a)

fmt.Printf(“b=(%T,%v)\n”, b, b)

fmt.Printf(“c=(%T,%v)\n”, c, c)

fmt.Printf(“d=(%T,%v)\n”, d, d)

useInt(12)

useFloat(12)

}

func useInt(n int) {

fmt.Printf(“useInt=(%T,%v)\n”, n, n)

}

func useFloat(n float64) {

fmt.Printf(“useFloat=(%T,%v)\n”, n, n)

}

运行结果:

var a int = 12

var b float64 = 12

var c interface{} = a

fmt.Println(“a==12:”, a == 12) // true

fmt.Println(“b==12:”, b == 12) // true

fmt.Println(“c==12:”, c == 12) // true

fmt.Println(“a==c:”, a == c) // true

fmt.Println(“b==c:”, b == c) // false

有没有一种抓狂的感觉。a等于12,b等于12,c等于12,a等于c,但是c不等于a。怎么回事?

我们看看之前我们得出的结论:

a=(int,12)

b=(float64,12)

c=(int,12)

看到这个是不是有点明白怎么回事了?类型不匹配啊!!!

另外有一个有意思的事情就是,当你拿一个数字和一个接口相比较时候,这个接口的类型永远是int;类似的当你硬编码的nil相比较时候,这个时候类型永远是<nil, nil>.

var b float64 = 12

var c interface{} = b

fmt.Println(“c==12:”, c == 12)

fmt.Printf(“c=(%T,%v)\n”, c, c)

fmt.Printf(“hard-coded=(%T,%v)\n”, 12, 12)

运行结果:

c==12: false

c=(float64,12)

hard-coded=(int,12)

总结

当我们硬编码的值和变量编译器做==操作时候,假定他们有一些特定类型和遵循一些规则来实现,指针的数据结构是<type,value>,比较时候会比较这两个参数。有时候这个情况会比较困惑,但是你只要去适应它就好了。如果你发现各种类型都可以分配给nil,避免问题的一种常用技术是显式地指定为nil。,而不是 a = b,例如:

var a *int = nil

var b interface{}

if a == nil {

  b = nil

}

这样我们对b进行硬编码赋值nil时候才可以得到我们想要的效果。

最后举一个常出现的例子

type AAer interface {

That() int

}

type BB struct {

}

type BBMgr struct {

bbMap map[int]AAer

}

func (this *BB) That() int {

return 0

}

func (this *BBMgr) QueryAA(id int32) AAer {

aa, _ :=this.bbMap[id]

//这里如果没有找到数据 返回的接口去做 nil比较的话

//发现 AAer永远不为nil,但是执行AAer的函数就报错

// 实际上这里应该写

// aa, ok :=this.bbMap[id]

// if !ok {

// return nil

// }

return aa

}

注:我同时维护了一个公众号,精选的内容我会推送到公众号里,你可以关注我的公众号同步获得这些精选内容。

发表评论

电子邮件地址不会被公开。 必填项已用*标注