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 在使用过程中却有不少的坑,需要特别注意。本文就记录一个nil
与interface{}
比较的问题。
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 的比较的主要内容,如果未能解决你的问题,请参考以下文章