前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go 神坑 1 —— interface{} 与 nil 的比较

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

作者头像
恋喵大鲤鱼
发布2021-12-06 11:09:14
4.5K1
发布2021-12-06 11:09:14
举报
文章被收录于专栏:C/C++基础

文章目录

1.前言

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

代码语言:javascript
复制
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 切片的比较问题。

代码语言:javascript
复制
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)
}

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

代码语言:javascript
复制
sl is nil
i is nil

但实际输出的结果是:

代码语言:javascript
复制
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的定义。

代码语言:javascript
复制
type eface struct { // 16 字节
	_type *_type
	data  unsafe.Pointer
}

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

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

代码语言:javascript
复制
// 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)
}

运行输出:

代码语言:javascript
复制
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我们可以找到。

代码语言:javascript
复制
type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

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

代码语言:javascript
复制
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)))
}

运行输出:

代码语言:javascript
复制
sl value is {array:<nil> len:0 cap:0}

不出所料,果然是零值。

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

4.避坑大法

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

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

我们能做的就是绕过此坑。如何绕过呢,有两个办法:

(1)既然值为 nil 的具型变量赋值给空接口会出现如此莫名其妙的情况,我们不要这么做,再赋值前先做判空处理,不为 nil 才赋给空接口;

(2)使用reflect.ValueOf().IsNil()来判断。不推荐这种做法,因为当空接口对应的具型是值类型,会 panic。

代码语言:javascript
复制
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 并发赋值的安全性探讨

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021/10/19 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 文章目录
  • 1.前言
  • 2.出乎意料的比较结果
  • 3.寻找问题所在
  • 4.避坑大法
  • 5.小结
  • 参考文献
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档