文章目录

  • 1.前言
  • 2.出乎意料的比较结果
  • 3.寻找问题所在
  • 4.避坑大法
  • 5.小结
  • 参考文献

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 []stringif 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 *_typedata  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 []stringIsNil(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.Pointerlen   intcap   int
}

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

type slice struct {array unsafe.Pointerlen   intcap   int
}func main() {var sl []stringfmt.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 []stringIsNil(sl)  // i is nilIsNil(nil)  // i is nil
}

5.小结

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

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

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


参考文献

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

Go 神坑 1 —— interface{} 与 nil 的比较相关推荐

  1. panic: interface conversion: interface {} is nil, not chan *sarama.ProducerError

    使用golang kafka sarama 包时,遇到如下问题: 高并发情况下使用同步sync producer,偶尔遇到crash: panic: interface conversion: int ...

  2. Go语言第一深坑 - interface 与 nil 的比较 (转)

    http://studygolang.com/articles/10635 转载于:https://www.cnblogs.com/majianguo/p/7426320.html

  3. [go]空nil与interface中的空指针

    文章目录 nil slice map interface 指针是否为空 Go语言中任何类型在未初始化时都对应一个零值:布尔类型是false,整型是0,字符串是"",而指针.函数.i ...

  4. golang 面试题(十三)interface内部结构和nil详解

    1.以下代码打印出来什么内容,说出为什么. package mainimport ("fmt" )type People interface {Show() }type Stude ...

  5. c++ 判断nil_golang A=nil,B=A,but B!=nil 这是真的

    一.现象 在某次开发过程中,想把参数验证放在middleware中封装达到提升开发效率的目的,结果出现了如下图所示的现象,验证返回的err明明是nil,但是用if判断的时候err却不是nil (图1 ...

  6. golang 接口类型 interface 简介

    目录 1. Go 语言与鸭子类型的关系 2. 值接收者和指针接收者的区别 方法 值接收者和指针接收者 两者分别在何时使用 3. iface 和 eface 的区别是什么 4. 接口的动态类型和动态值 ...

  7. golang 反射_golang原理篇- nil:接口类型和值类型的区别

    interface接口类型是golang的最重要的数据结构,底层是value和type组成,实现interface的struct的实例都能赋值给接口类型的变量,实现动态value的能力.type记录v ...

  8. 理解Go Interface

    理解Go Interface 1 概述 Go语言中的接口很特别,而且提供了难以置信的一系列灵活性和抽象性.接口是一个自定义类型,它是一组方法的集合,要有方法为接口类型就被认为是该接口.从定义上来看,接 ...

  9. golang 接口类型 interface 简介使用

    1. Go 语言与鸭子类型的关系 先直接来看维基百科里的定义: If it looks like a duck, swims like a duck, and quacks like a duck, ...

最新文章

  1. iec61850采样协议(9-1、9-2)解析(二)
  2. uniapp光标自动定义到文本框_如何在Mac上的照片应用中创建自定义日历
  3. Java JNI简单实现
  4. Java各进制之间的转换
  5. HTML+CSS+JS实现 ❤️canvas 3D立体图片相册幻灯片❤️
  6. GradView使用举例
  7. python爬虫代码示例 动态_python动态爬虫的实例分享
  8. jetson tx2上运行mobilenet-ssd的坑:interrupted by signal 9: SIGKILL
  9. 通用即插即用监视器驱动下载_驱动之家和驱动精灵哪个好
  10. 基于Android的海康威视的二次开发
  11. 动态电路电容电感充放电分析
  12. 支持向量机(股票)——Python量化
  13. android 图片释放内存吗,手机内存不足?掌握这几招让手机瞬间释放几个G!
  14. HDU-4417-Super Mario(划分树+二分)
  15. centos8重启网卡命令nmcli
  16. c语言双字节异或,C语言 按位异或实现加法
  17. VC#复习资料(是轩~)
  18. arch linux添加用户,Arch Linux配置教程
  19. 微信小程序制作科学计算器(控制台接受显示数据)
  20. Windows 使用技巧

热门文章

  1. 推进 DevSecOps 走向未来
  2. 关于Java获取系统信息
  3. 看看20万码农怎么评论:女生做程序员是一种怎样的体验?
  4. 英国脱欧:3/4的技术初创公司将面临严峻时期
  5. Mirantis决定采用Kubernetes作为 Openstack的生命周期管理工具
  6. Mycat 设置全局序列号
  7. 保险公司信息系统审计刍议
  8. 用simple from暂不用formtastic
  9. python 进制间相互转换
  10. PAT 乙级 1054. 求平均值 (20) Java版