go之nil

2019/12/24 Go 共 2943 字,约 9 分钟

小结

Go 语言中有两种略微不同的接口,一种是带有一组方法的接口,另一种是不带任何方法的 interface{}:

golang-different-interface

需要注意的是,与 C 语言中的 void * 不同,interface{} 类型不是任意类型。如果我们将类型转换成了 interface{} 类型,变量在运行期间的类型也会发生变化,获取变量类型时会得到 interface{}。

  • 接口(interface) 是对非接口值(例如指针,struct等)的封装,内部实现包含 2 个字段,类型 T 和 值 V。一个接口等于 nil,当且仅当 T 和 V 处于 unset 状态(T=nil,V is unset)。
  • 接口和其它变量,会把其它变量转换成接口,然后再进行比较。仅当T和V都相等时相等。
  • 把一个带类型的变量赋值给interface,则interface不是nil,因此T是非nil的。

正文

nil是一种数据结构么?为什么会讲到它,没搞错吧?没搞错。不仅仅是Go语言中,每门语言中nil都是非常重要的,它代表的是空值的语义。

在不同语言中,表示空这个概念都有细微不同。比如在scheme语言(一种lisp方言)中,nil是true的!而在ruby语言中,一切都是对象,连nil也是一个对象!在C中NULL跟0是等价的。

按照Go语言规范,任何类型在未初始化时都对应一个零值:布尔类型是false,整型是0,字符串是”“,而指针,函数,interface,slice,channel和map的零值都是nil。

接口变量的值在什么情况下才真正为nil

一个interface在没有进行初始化时,对应的值是nil。也就是说var v interface{},

此时v就是一个nil。在底层存储上,它是一个空指针。与之不同的情况是,interface值为空。比如:

var v *T
var i interface{}
i = v

此时i是一个interface,它的值是nil,但它自身不为nil。

这个问题初看起来就不是个问题。对于一个引用类型的变量,它的值是否为nil完全取决于我们赋给它了什么,是这样吗?我们先来看一段代码:

	var dog1 *Dog
	fmt.Println("The first dog is nil. [wrap1]")
	dog2 := dog1
	fmt.Println("The second dog is nil. [wrap1]")
	var pet Pet = dog2
	if pet == nil {
		fmt.Println("The pet is nil. [wrap1]")
	} else {
		fmt.Println("The pet is not nil. [wrap1]")
	}

在 demo33.go 文件的这段代码中,我先声明了一个*Dog类型的变量dog1,并且没有对它进行初始化。这时该变量的值是什么?显然是nil。然后我把该变量赋给了dog2,后者的值此时也必定是nil,对吗?

现在问题来了:当我把dog2赋给Pet类型的变量pet之后,变量pet的值会是什么?答案是nil吗?

如果你真正理解了我在上一个问题的解析中讲到的知识,尤其是接口变量赋值及其值的数据结构那部分,那么这道题就不难回答。你可以先思考一下,然后再接着往下看。

当我们把dog2的值赋给变量pet的时候,dog2的值会先被复制,不过由于在这里它的值是nil,所以就没必要复制了。

然后,Go 语言会用我上面提到的那个专用数据结构iface的实例包装这个dog2的值的副本,这里是nil。

虽然被包装的动态值是nil,但是pet的值却不会是nil,因为这个动态值只是pet值的一部分而已。

顺便说一句,这时的pet的动态类型就存在了,是*Dog。我们可以通过fmt.Printf函数和占位符%T来验证这一点,另外reflect包的TypeOf函数也可以起到类似的作用。

换个角度来看。我们把nil赋给了pet,但是pet的值却不是nil。

这很奇怪对吗?其实不然。在 Go 语言中,我们把由字面量nil表示的值叫做无类型的nil。这是真正的nil,因为它的类型也是nil的。

虽然dog2的值是真正的nil,但是当我们把这个变量赋给pet的时候,Go 语言会把它的类型和值放在一起考虑。

也就是说,这时 Go 语言会识别出赋予pet的值是一个*Dog类型的nil。然后,Go 语言就会用一个iface的实例包装它,包装后的产物肯定就不是nil了。

只要我们把一个有类型的nil赋给接口变量,那么这个变量的值就一定不会是那个真正的nil。因此,当我们使用判等符号==判断pet是否与字面量nil相等的时候,答案一定会是false。

那么,怎样才能让一个接口变量的值真正为nil呢?要么只声明它但不做初始化,要么直接把字面量nil赋给它。

type Error struct {
}

func main() {
	var a interface{}
	a = nil
	println(a == nil)
	var err Error
	a = err
	println(a == nil)
}

另一个error的例子

Go中的error其实就是一个实现了Error方法的接口:

type error interface {
    Error() string
}

因此,我们可以自定义一个error:

type Error struct {
    errCode uint8
}
func (e *Error) Error() string {
        switch e.errCode {
        case 1:
                return "file not found"
        case 2:
                return "time out"
        case 3:
                return "permission denied"
        default:
                return "unknown error"
         }
}

如果我们这样使用它:

func checkError(err error) {
    if err != nil {
        panic(err)
    }
}
var e *Error
checkError(e)

e是nil的,但是当我们checkError时就会panic。请读者思考一下为什么?

总之,interface跟C语言的指针一样非常灵活,关于空的语义,也跟空指针一样容易困扰新手的,需要注意。

Go中两个Nil可能不相等吗?

Go中两个Nil可能不相等。

接口(interface) 是对非接口值(例如指针,struct等)的封装,内部实现包含 2 个字段,类型 T 和 值 V。一个接口等于 nil,当且仅当 T 和 V 处于 unset 状态(T=nil,V is unset)。

两个接口值比较时,会先比较 T,再比较 V。 接口值与非接口值比较时,会先将非接口值尝试转换为接口值,再比较。

func main() {
	var p *int = nil
	var i interface{} = p
	fmt.Println(i == p) // true
	fmt.Println(p == nil) // true
	fmt.Println(i == nil) // false
}
复制代码
  • 例子中,将一个nil非接口值p赋值给接口i,此时,i的内部字段为(T=*int, V=nil),i与p作比较时,将 p 转换为接口后再比较,因此 i == p,p 与 nil 比较,直接比较值,所以 p == nil。
  • 但是当 i 与nil比较时,会将nil转换为接口(T=nil, V=nil),与i(T=*int, V=nil)不相等,因此 i != nil。因此 V 为 nil ,但 T 不为 nil 的接口不等于 nil。

Search

    Table of Contents