Go 神坑 1 —— interface{} 与 nil 的比较

Posted 恋喵大鲤鱼

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go 神坑 1 —— interface{} 与 nil 的比较相关的知识,希望对你有一定的参考价值。


1.前言

interface 是 Go 里所提供的非常重要的特性。一个 interface 里可以定义一个或者多个函数,例如系统自带的io.ReadWriter的定义如下所示:

type ReadWriter interface {
    Read(b []byte) (n int, err error)
    Write(b []byte) (n int, err error)
}

任何类型只要它提供了 Read 和 Write 的实现,那么这个类型便实现了这个 interface(duck-type),而不像 Java 需要开发者使用 implements 标明。

然而 Go 的 interface 在使用过程中却有不少的坑,需要特别注意。本文就记录一个nilinterface{}比较的问题。

2.出乎意料的比较结果

首先看一个比较 nil 切片的比较问题。

func IsNil(i interface{}) {
	if i == nil {
		fmt.Println("i is nil")
		return
	}
	fmt.Println("i isn't nil")
}

func main() {
	var sl []string
	if sl == nil {
		fmt.Println("sl is nil")
	}
	IsNil(sl)
}

运行上面代码输出结果是怎样的呢?你可能我和我一样,很自信认为输出是下面这样的:

sl is nil
i is nil

但实际输出的结果是:

sl is nil
i isn't nil

Oh my god,为啥一个nil切片经过空接口interface{}一中转,就变成了非 nil。

3.寻找问题所在

想要理解这个问题,首先需要理解 interface{} 变量的本质。

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

Go 语言使用runtime.iface表示带方法的接口,使用runtime.eface表示不带任何方法的空接口interface{}

一个 interface{} 类型的变量包含了 2 个指针,一个指针指向值的类型,另外一个指针指向实际的值。在 Go 源码中 runtime 包下,我们可以找到runtime.eface的定义。

type eface struct { // 16 字节
	_type *_type
	data  unsafe.Pointer
}

从空接口的定义可以看到,当一个空接口变量为 nil 时,需要其两个指针均为 0 才行。

回到最初的问题,我们打印下传入函数中的空接口变量值,来看看它两个指针值的情况。

// InterfaceStruct 定义了一个 interface{} 的内部结构
type InterfaceStruct struct {
	pt uintptr // 到值类型的指针
	pv uintptr // 到值内容的指针
}

// ToInterfaceStruct 将一个 interface{} 转换为 InterfaceStruct
func ToInterfaceStruct(i interface{}) InterfaceStruct {
	return *(*InterfaceStruct)(unsafe.Pointer(&i))
}

func IsNil(i interface{}) {
	fmt.Printf("i value is %+v\\n", ToInterfaceStruct(i))
}

func main() {
	var sl []string
	IsNil(sl)
	IsNil(nil)
}

运行输出:

i value is {pt:6769760 pv:824635080536}
i value is {pt:0 pv:0}

可见,虽然 sl 是 nil 切片,但是其本上是一个类型为 []string,值为空结构体 slice 的一个变量,所以 sl 传给空接口时是一个非 nil 变量。

再细究的话,你可能会问,既然 sl 是一个有类型有值的切片,为什么又是个 nil。

针对具体类型的变量,判断是否是 nil 要根据其值是否为零值。因为 sl 一个切片类型,而切片类型的定义在源码包src/runtime/slice.go我们可以找到。

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

我们继续看一下值为 nil 的切片对应的 slice 是否为零值。

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

func main() {
	var sl []string
	fmt.Printf("sl value is %+v\\n", *(*slice)(unsafe.Pointer(&sl)))
}

运行输出:

sl value is {array:<nil> len:0 cap:0}

不出所料,果然是零值。

至此解释了开篇出乎意料的比较结果背后的原因:空切片为 nil 因为其值为零值,类型为 []string 的空切片传给空接口后,因为空接口的值并不是零值,所以接口变量不是 nil

4.避坑大法

知道前面有个坑,如何避免不掉进去。我们想到的做法无非就两种:一填坑,二绕过。

因为这是由 Go 的特性决定,直白点就是设计如此。面对此坑,我们开发者只能望洋兴叹,无可奈何,留给 Go 核心团队那帮天才来填坑吧。

我们能做的就是绕过此坑。如何绕过呢,有两个办法:
(1)既然值为 nil 的具型变量赋值给空接口会出现如此莫名其妙的情况,我们不要这么做,再赋值前先做判空处理,不为 nil 才赋给空接口;
(2)使用reflect.ValueOf().IsNil()来判断。不推荐这种做法,因为当空接口对应的具型是值类型,会 panic。

func IsNil(i interface{}) {
	if i != nil {
		if reflect.ValueOf(i).IsNil() {
			fmt.Println("i is nil")
			return
		}
		fmt.Println("i isn't nil")
	}
	fmt.Println("i is nil")
}

func main() {
	var sl []string
	IsNil(sl)	// i is nil
	IsNil(nil)  // i is nil
}

5.小结

Go 简单高效,功能强大,如此优秀的语言也存在很多让人误用的特性,本文便记录万坑之一的 nil 赋给空接口时判空失效的问题,后面会继续给出 Go 使用的常见问题,帮助大家少踩坑。

切记,Go 中变量是否为 nil 要看变量的值是否是零值。

切记,不要将值为 nil 的变量赋给空接口。


参考文献

Golang 接口相等比较注意要点
Go语言第一深坑 - interface 与 nil 的比较
Go 语言设计与实现.4.2接口
Golang 并发赋值的安全性探讨

以上是关于Go 神坑 1 —— interface{} 与 nil 的比较的主要内容,如果未能解决你的问题,请参考以下文章

Go进阶详解接口 interface

Go之interface

Go之interface

Go之interface

Go语言中的Interface

Go语言中不一样的Interface