[翻译]When nil Isn't Equal to nil

原文:https://www.calhoun.io/when-nil-isnt-equal-to-nil

本文源于 a question asked on the Go Forums,是我回复的版本的略微修改。

在本文中,我们将探讨一些情况,某些变量看起来相等,当使用Golang的==比较时却不等。接下来我们将探讨这种情况发生的原因,并且如何轻松的在自己的代码中避免遇到这种问题。

首先让我们看一个例子。假设有两个变量,每个变量都有自己的类型,且每个变量都分配了硬编码值nil

1
2
var a *int = nil
var b interface{} = nil

你觉得下面代码的结果什么呢?

1
2
3
fmt.Println("a == nil:", a == nil)
fmt.Println("b == nil:", b == nil)
fmt.Println("a == b:", a == b)

可以运行上面代码来验证你的结果。事实上正确结果如下:

1
2
3
a == nil: true
b == nil: true
a == b: false

我们快速的来看另一个相似而不相同的例子。接下来中会更改变量b的初始值:

1
2
3
4
5
6
7
8
var a *int = nil
// 这是唯一的变化,我们使用a来代替硬编码的nil值赋值给a
var b interface{} = a

fmt.Println("a == nil:", a == nil)
fmt.Println("b == nil:", b == nil)
fmt.Println("a == b:", a == b)
}

再次,你觉得上面代码的输出是什么呢?下面给出正确答案:

1
2
3
a == niltrue
b == nilfalse
a == b:true

究竟发生了什么呢?

这中情况有点难(hard/weird)解释。但是这个不是bug,也不是其他(random/magical/whatever)。这儿只是有一些被声明的明确的规则,我们需要花点时间来理解。接下来你就会明白,偶尔看到别人的代码写成如下样子:

1
2
3
if a == nil {
b = nil
}

代替使用a来赋值给b

首先我们需要理解Go中的每个指针都有两个基本信息:类型和指针指向的值。下面将用(type, value)来表示。

因为每个指针变量都需要一个类型,所以不能在不声明类型的情况下将nil值赋给变量。因此,下面代码不会编译通过:

1
2
// 因为无法确认n的类型,因此编译不通过
n := nil

为了编译上面的代码,我们必须使用一个有类型的指针,并赋值nil

1
2
var a *int = nil
var b interface{} = nil

现在,两个变量都有类型了。可以使用fmt.Printf来打印出他们的类型。

1
2
3
4
5
var a *int = nil
var b interface{} = nil

fmt.Printf("a=(%T, ...)\n", a)
fmt.Printf("b=(%T, ...)\n", b)

注意:%T用于打印值的类型fmt.Printf。你可以在文档中阅读有关这些特殊字符的更多信息。

输出结果如下:

1
2
a=(*int, ...)
b=(<nil>, ...)

看起来,将nil硬编码给*int类型的变量a时,被设置为和变量a相同类型——*int。那就有意义了。

第二个变量b有些令人困惑。它的类型是interface{}空接口),但是打印nil值的类型,却是<nil>。发生了什么呢?

简单的说,就是因为我们使用空接口,任何类型都可以适用。<nil>类型严格上也是一种类型,它可以适用于空接口,因此在编译器没有其他类型信息使用时就会生效。

所以,我们知道所有的指针都有(type, value)两个属性,并且已经看过了当nil赋值给某个变量时会发生什么。那么接下来看看通过a变量而不是nil来赋值b后,它们的类型都是什么?

1
2
3
4
5
var a *int = nil
var b interface{} = a

fmt.Printf("a=(%T, ...)\n", a)
fmt.Printf("b=(%T, ...)\n", b)

Huh…看起来b有一个新类型了。

当给b硬编码nil之前,没有类型信息。使用ab赋值时就不是这样了。通过变量a能够准确的知道应该使用什么类型。

快速总结

  • 所有的指针都有值和类型;
  • 当给一个变量硬编码nil值时,编译器会决定用什么正确的类型;
  • 当用一个nil的变量赋值给当前对象时,可以通过之前的变量来决定当前的类型。

当检查相等时发生来什么?

我们理解类型的决定方式了,那么我们看看判等时发生了什么。

首先有两个都使用nil硬编码赋值的变量ab。接下来就是类似上面a赋值给b的桥段了。

1
2
3
4
5
6
7
8
9
10
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)

接下来是输出结果如下:

1
2
3
4
5
6
a=(*int, <nil>)
b=(<nil>, <nil>)

a == nil: true
b == nil: true
a == b: false

显然最奇怪的地方就是a不等于b。这看起来特别奇怪,因为乍眼一看,a == nil并且b == nila != b,这在逻辑上根本不可能。

实际是下面这种情况——举个例子,a == nil——并不能正确代表比较的结果。真正比较的是它们两个变量的值和类型。也就是说,不仅比较存储在anil上的值,也比较它们的类型。确切的结果展示如下:

1
2
3
4
5
a == nil: (*int, <nil>) == (*int*, <nil>)
b == nil: (<nil>, <nil>) == (<nil>, <nil>)
# 注意:这两个很明显不相同
# 因此我们加上了类型信息
a == b: (*int, <nil>) == (<nil>, <nil>)

当通过这种方式记下这些比较后,两个变量就变得很明显不相等了——它们拥有不同的类型——但是这些信息在代码里并不是特别清晰,因此很不幸导致众多的误解。

一个供选择的方法
如果你真的想在你的代码里比较ab,你可以使用下面的代码来代替你想写的:

1
2
3
if a == nil && b == nil {
// both are nil!
}

这需要更多的代码,但是能更好的表达你的意图。也就是说,这种方法可以通过将另一个nil变量(而不是硬编码nil)赋值给b,将在接下来的例子看到。

现在来看看将a赋值给b并且执行相同的比较时,会发生什么。

1
2
3
4
5
6
7
8
9
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)

这个程序的输出是:

1
2
3
4
5
6
a=(*int, <nil>)
b=(*int, <nil>)

a == nil: true
b == nil: false
a == b: true

奇怪的是第二行,b == nil

这有点不明显,当将b和硬编码的nil值比较时,编译器也需要决定nil的类型。这种情况下,如果赋值nil给变量b时,编译器会做出相同的决定——也就是将等号的右边设置为(<nil>, <nil>)——如果查看b的输出时,明显有不同的类型:(*int, <nil>)

这点上大家比较认同的是,这儿非常令人困惑,语言本身应该为我们处理这个细节。不幸的是这个在编译时不可能,因为b的世纪类型可以随着程序运行而改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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变量的类型改变来3次。刚开始是(*int, <nil>),后来变成(*string, <nil>),最后变成了(<nil>, <nil>)

这种改变三次的类型编译器编译时无法判断,因此在Go里只能处理成运行时动态处理,它有一些独特的难题,可能不值得介绍。

编译时类型决定也可以用数字来演示

我们看到nil如何别强制转变成正确的类型,但是这不是编译器决定正确的类型的唯一情况。例如,当将硬编码的数赋值给变量,编译器将基于程序的上下文决定使用哪种类型。

显而易见的一种情况,就是当类型与变量一起声明(例如var a int = 12),但是这种情况也发生在将硬编码值传递给函数或者只是将数字赋值给变量时。这些情况都会在下面的代码中展示。

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
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)
}

我们可以用一些数字来展示这些困惑。

1
2
3
4
5
6
7
8
9
10
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和b的类型不匹配,我们不能比较它们

现在a == 12, b == 12, c == 12, 但是当我们比较b == c是得到false。什么!!!

再次去理解它们的类型:

1
2
3
a=(int,12)
b=(float64,12)
c=(int,12)

acint类型。bfloat64类型,因此它们和硬编码值12进行比较时,在比较前,需要把比较的两边强制转化成同一类型。

对于数字,另一个有趣的是,当12和一个接口进行比较时,编译器会一直将它强制转换成int类型。类似于,nil和接口类型比较时,怎么转化成(<nil>, <nil>)
,修改代码演示证明:

1
2
3
4
5
6
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)

输出下列结果:

1
2
3
c==12: false
c=(float64,12)
hard-coded=(int,12)

现在c == 12返回false,因为(float64, 12)和硬编码的(int, 12)是不同的,因为它们有不同的类型。

总结

当硬编码值与变量比较时,编译器必须假设它们有某种特定类型并遵循一些规则来实现这一点。有时这很困惑,但慢慢的你就会习惯的。

如果发现自己使用的类型都可以使用nil赋值时,一种常用的避免问题方法就是明确地赋值nil。也就是,代替a = b写成下面这样:

1
2
3
4
5
6
var a *int = nil
var b interface{}

if a == nil {
b = nil
}

接下来将b和硬编码的nil比较时,就会得到预期的结果。这需要更多的代码,但它几乎在所有情况下都会得到想要的结果。

声明
我没有研究任何实际编译器或Go的内部工作原理,所以如果有不准确的,请告诉我,我会解决它。这篇文章完全基于我看到过或读过的其他文章。


译者注:
本文是我在遇到Golang的nil问题时找到的,本文的翻译可能不够完美,有需要纠正的地方请提出,我会改正。