基于go interface{}==nil 的几种坑及原理分析


Posted in Golang onApril 24, 2021

本文是Go比较有名的一个坑,在以前面试的时候也被问过,为什么想起来写这个?

因为我们线上就真实出现过这个坑,写给不了解的人在使用 if err != nil 的时候提高警惕。

Go语言的interface{}在使用过程中有一个特别坑的特性,当你比较一个interface{}类型的值是否是nil的时候,这是需要特别注意避免的问题。

先来看看一个demo:

package main
import "fmt"
type ErrorImpl struct{}
func (e *ErrorImpl) Error() string {
   return ""
}
var ei *ErrorImpl
var e error
func ErrorImplFun() error {
   return ei
}
func main() {
   f := ErrorImplFun()
   fmt.Println(f == nil)
}

输出:

false

为什么不是true?

想要理解这个问题,首先需要理解interface{}变量的本质。在Go语言中,一个interface{}类型的变量包含了2个指针,一个指针指向值的在编译时确定的类型,另外一个指针指向实际的值。

// InterfaceStructure 定义了一个interface{}的内部结构
type InterfaceStructure struct {
  pt uintptr // 到值类型的指针
  pv uintptr // 到值内容的指针
}
// asInterfaceStructure 将一个interface{}转换为InterfaceStructure
func asInterfaceStructure(i interface{}) InterfaceStructure {
  return *(*InterfaceStructure)(unsafe.Pointer(&i))
}
func main() {
  var i1, i2 interface{}
  var v1 int = 23
  var v2 int = 23
  i1 = v1
  i2 = v2
  fmt.Printf("sizeof interface{} = %d\n", unsafe.Sizeof(i1))
  fmt.Printf("i1 %v %+v\n", i1, asInterfaceStructure(i1))
  fmt.Printf("i2 %v %+v\n", i2, asInterfaceStructure(i2))
  var nilInterface interface{}
  var str *string
  fmt.Printf("nil interface = %+v\n", asInterfaceStructure(nilInterface))
  fmt.Printf("nil string = %+v\n", asInterfaceStructure(str))
  fmt.Printf("nil = %+v\n", asInterfaceStructure(nil))
}

输出:

sizeof interface{} = 16

i1 23 {pt:4812032 pv:825741246928}

i2 23 {pt:4812032 pv:825741246936}

nil interface = {pt:0 pv:0}

nil string = {pt:4802400 pv:0}

nil = {pt:0 pv:0}

当我们将一个具体类型的值赋值给一个interface{}类型的变量的时候,就同时把类型和值都赋值给了interface{}里的两个指针。如果这个具体类型的值是nil的话,interface{}变量依然会存储对应的类型指针和值指针。

如何解决?

方法一

返回的结果进行非nil检查,然后再赋值给interface{}变量

type ErrorImpl struct{}
func (e *ErrorImpl) Error() string {
   return ""
}
var ei *ErrorImpl
var e error
func ErrorImplFun() error {
   if ei == nil {
      return nil
   }
   return ei
}
func main() {
   f := ErrorImplFun()
   fmt.Println(f == nil)
}

输出:

true

方法二

返回具体实现的类型而不是interface{}

package main
import "fmt"
type ErrorImpl struct{}
func (e *ErrorImpl) Error() string {
   return ""
}
var ei *ErrorImpl
var e error
func ErrorImplFun() *ErrorImpl {
   return ei
}
func main() {
   f := ErrorImplFun()
   fmt.Println(f == nil)
}

输出:

true

解决由于第三方包带来的坑

由于有的error是第三方包返回的,又自己不想改第三方包,只好接收处理的时候想办法。

方法一

利用interface{}原理

is:=*(*InterfaceStructure)(unsafe.Pointer(&i))
 if is.pt==0 && is.pv==0 {
     //is nil do something
 }

将底层指向值和指向值的类型的指针打印出来如果都是0,表示是nil

方法二

利用断言,断言出来具体类型再判断非空

type ErrorImpl struct{}
func (e ErrorImpl) Error() string {
   return "demo"
}
var ei *ErrorImpl
var e error
func ErrorImplFun() error {
   //ei = &ErrorImpl{}
   return ei
}
func main() {
   f := ErrorImplFun()
   //当然error实现类型较多的话使用  
 //switch case方式断言更清晰
   res, ok := f.(*ErrorImpl)
   fmt.Printf("ok:%v,f:%v,res:%v", 
   ok, f == nil, res == nil)
}

输出:

ok:true,f:false,res:true

方法三

利用反射

type ErrorImpl struct{}
func (e ErrorImpl) Error() string {
   return "demo"
}
var ei *ErrorImpl
var e error
func ErrorImplFun() error {
   //ei = &ErrorImpl{}
   return ei
}
func main() {
   f := ErrorImplFun()
   rv := reflect.ValueOf(f)
   fmt.Printf("%v", rv.IsNil())
}

输出:

true

注意⚠:

断言和反射性能不是特别好,如果不得已再使用,控制使用有助于提升程序性能。

由于函数接收类型导致的panic:

type ErrorImpl struct{}
func (e ErrorImpl) Error() string {
   return "demo"
}
var ei *ErrorImpl
var e error
func ErrorImplFun() error {
   return ei
}
func main() {
   f := ErrorImplFun()
   fmt.Printf(f.Error())
}

输出:

panic: value method main.ErrorImpl.Error called using nil *ErrorImpl pointer

解决:

func (e *ErrorImpl) Error() string {
   return "demo"
}

输出:

demo

可以发现将接收类型变成指针类型就可以了。

以上就是 nil 相关的坑,希望大家可以牢记,如果 ”幸运“ 的遇到了,可以想到这些可能性。

补充:go 语言 interface{} 的易错点

如果说 goroutine 和 channel 是 go 语言并发的两大基石,那 interface 就是 go 语言类型抽象的关键。

在实际项目中,几乎所有的数据结构最底层都是接口类型。

说起 C++ 语言,我们立即能想到是三个名词:封装、继承、多态。go 语言虽然没有严格意义上的对象,但通过 interface,可以说是实现了多态性。(由以组合结构体实现了封装、继承的特性)

package main
type animal interface {
    Move()
}
type bird struct{}
func (self *bird) Move() {
    println("bird move")
}
type beast struct{}
func (self *beast) Move() {
    println("beast move")
}
func animalMove(v animal) {
    v.Move()
}
func main() {
    var a *bird
    var b *beast
    animalMove(a) // bird move
    animalMove(b) // beast move
}

go 语言中支持将 method、struct、struct 中成员定义为 interface 类型,使用 struct 举一个简单的栗子

使用 go 语言的 interface 特性,就能实现多态性,进行泛型编程。

二,interface 原理

如果没有充分了解 interface 的本质,就直接使用,那最终肯定会踩到很深的坑,要用就先要了解,先来看看 interface 源码

type eface struct {
     _type *_type
     data  unsafe.Pointer
 }  
 type _type struct {
     size       uintptr // type size
     ptrdata    uintptr // size of memory prefix holding all pointers
     hash       uint32  // hash of type; avoids computation in hash tables
     tflag      tflag   // extra type information flags
     align      uint8   // alignment of variable with this type
     fieldalign uint8   // alignment of struct field with this type
     kind       uint8   // enumeration for C
     alg        *typeAlg  // algorithm table
     gcdata    *byte    // garbage collection data
     str       nameOff  // string form
     ptrToThis typeOff  // type for pointer to this type, may be zero
 }

可以看到 interface 变量之所以可以接收任何类型变量,是因为其本质是一个对象,并记录其类型和数据块的指针。(其实 interface 的源码还包含函数结构和内存分布,由于不是本文重点,有兴趣的同学可以自行了解)

三,interface 判空的坑

对于一个空对象,我们往往通过 if v == nil 的条件语句判断其是否为空,但在代码中充斥着 interface 类型的情况下,很多时候判空都并不是我们想要的结果(其实了解或聪明的同学从上述 interface 的本质是对象已经知道我想要说的是什么)

package main 
 type animal interface {
     Move()
 } 
 type bird struct{} 
 func (self *bird) Move() {
     println("bird move")
 } 
 type beast struct{} 
 func (self *beast) Move() {
     println("beast move")
 } 
 func animalMove(v animal) {
     if v == nil {
         println("nil animal")
     }
     v.Move()
 } 
 func main() {
     var a *bird   // nil
     var b *beast  // nil
     animalMove(a) // bird move
     animalMove(b) // beast move
 }

还是刚才的栗子,其实在 go 语言中 var a *bird 这种写法,a 只是声明了其类型,但并没有申请一块空间,所以这时候 a 本质还是指向空指针,但我们在 aminalMove 函数进行判空是失败的,并且下面的 v.Move() 的调用也是成功的,本质的原因就是因为 interface 是一个对象,在进行函数调用的时候,就会将 bird 类型的空指针进行隐式转换,转换成实例的 interface animal 对象,所以这时候 v 其实并不是空,而是其 data 变量指向了空。

这时候看着执行都正常,那什么情况下坑才会绊倒我们呢?只需要加一段代码

package main 
 type animal interface {
     Move()
 } 
 type bird struct {
    name string
 } 
 func (self *bird) Move() {
     println("bird move %s", self.name) // panic
 } 
 type beast struct {
     name string
 } 
 func (self *beast) Move() {
     println("beast move %s", self.name) // panic
 } 
 func animalMove(v animal) {
     if v == nil {
         println("nil animal")
     }
     v.Move()
 } 
 func main() {
     var a *bird   // nil
     var b *beast  // nil
     animalMove(a) // panic
     animalMove(b) // panic
 }

在代码中,我们给派生类添加 name 变量,并在函数的实现中进行调用,就会发生 panic,这时候的 self 其实是 nil 指针。所以这里坑就出来了。

有些人觉得这类错误谨慎一些还是可以避免的,那是因为我们是正向思维去代入接口,但如果反向编程就容易造成很难发现的 bug

package main 
 type animal interface {
     Move()
 } 
 type bird struct {
     name string
 } 
 func (self *bird) Move() {
     println("bird move %s", self.name)
 } 
 type beast struct {
     name string
 } 
 func (self *beast) Move() {
     println("beast move %s", self.name)
 } 
 func animalMove(v animal) {
     if v == nil {
         println("nil animal")
     }
     v.Move()
 } 
 func getBirdAnimal(name string) *bird {
     if name != "" {
         return &bird{name: name}
     }
     return nil
 } 
 func main() {
     var a animal
     var b animal
     a = getBirdAnimal("big bird")
     b = getBirdAnimal("") // return interface{data:nil}
     animalMove(a) // bird move big bird
     animalMove(b) // panic
 }

这里我们看到通过函数返回实例类型指针,当返回 nil 时,因为接收的变量为接口类型,所以进行了隐性转换再次导致了 panic(这类反向转换很难发现)。

那我们如何处理上述这类问题呢。我这边整理了三个点

1,充分了解 interface 原理,使用过程中需要谨慎小心

2,谨慎使用泛型编程,接收变量使用接口类型,也需要保证接口返回为接口类型,而不应该是实例类型

3,判空是使用反射 typeOf 和 valueOf 转换成实例对象后再进行判空

Golang 相关文章推荐
goland 恢复已更改文件的操作
Apr 28 Golang
对Golang中的FORM相关字段理解
May 02 Golang
解决goland 导入项目后import里的包报红问题
May 06 Golang
Golang 获取文件md5校验的方法以及效率对比
May 08 Golang
浅谈Go语言多态的实现与interface使用
Jun 16 Golang
golang中字符串MD5生成方式总结
Jul 04 Golang
Golang表示枚举类型的详细讲解
Sep 04 Golang
golang实现浏览器导出excel文件功能
Mar 25 Golang
简单聊聊Golang中defer预计算参数
Mar 25 Golang
GO语言异常处理分析 err接口及defer延迟
Apr 14 Golang
详解Go语言中Get/Post请求测试
Jun 01 Golang
GoFrame框架数据校验之校验结果Error接口对象
Jun 21 Golang
golang interface判断为空nil的实现代码
Apr 24 #Golang
golang判断key是否在map中的代码
Apr 24 #Golang
Go语言操作数据库及其常规操作的示例代码
Apr 21 #Golang
为什么不建议在go项目中使用init()
Apr 12 #Golang
Golang二维切片初始化的实现
Apr 08 #Golang
go语言map与string的相互转换的实现
Apr 07 #Golang
一文读懂go中semaphore(信号量)源码
Apr 03 #Golang
You might like
php定时删除文件夹下文件(清理缓存文件)
2013/01/23 PHP
如何使用Strace调试工具
2013/06/03 PHP
解析PHP工厂模式的好处
2013/06/18 PHP
调整PHP的性能
2013/10/30 PHP
Cygwin中安装PHP方法步骤
2015/07/04 PHP
php获取ajax的headers方法与内容实例
2017/12/27 PHP
PHP解密支付宝小程序的加密数据、手机号的示例代码
2021/02/26 PHP
用JS剩余字数计算的代码
2008/07/03 Javascript
js URL参数的拼接方法比较
2012/02/15 Javascript
jquery遍历筛选数组的几种方法和遍历解析json对象
2013/12/13 Javascript
javaScript基础语法介绍
2015/02/28 Javascript
jQuery模拟物体自由落体运动(附演示与demo源码下载)
2016/01/21 Javascript
JavaScript数值千分位格式化的两种简单实现方法
2016/08/01 Javascript
canvas知识总结
2017/01/25 Javascript
vue.js中Vue-router 2.0基础实践教程
2017/05/08 Javascript
vue如何获取点击事件源的方法
2017/08/10 Javascript
Vue cli 引入第三方JS和CSS的常用方法分享
2018/01/20 Javascript
Nuxt配合Node在实际生产中的应用详解
2018/08/07 Javascript
js防抖函数和节流函数使用场景和实现区别示例分析
2020/04/11 Javascript
JS轮播图的实现方法
2020/08/24 Javascript
[02:53]DOTA2英雄基础教程 山岭巨人小小
2013/12/09 DOTA
[54:56]DOTA2上海特级锦标赛主赛事日 - 5 总决赛Liquid VS Secret第三局
2016/03/06 DOTA
[05:53]敌法师的金色冠名ID"BurNIng",是传说,是荣耀
2020/07/11 DOTA
python3.5仿微软计算器程序
2020/03/30 Python
解决python ogr shp字段写入中文乱码的问题
2018/12/31 Python
python random从集合中随机选择元素的方法
2019/01/23 Python
解决Python selenium get页面很慢时的问题
2019/01/30 Python
Python实现FTP文件传输的实例
2019/07/07 Python
解决python和pycharm安装gmpy2 出现ERROR的问题
2020/08/28 Python
Python 随机按键模拟2小时
2020/12/30 Python
基于Python-Pycharm实现的猴子摘桃小游戏(源代码)
2021/02/20 Python
应聘医药代表职位求职信
2013/10/21 职场文书
中专毕业生自我鉴定范文
2013/11/09 职场文书
校园环保标语
2014/06/13 职场文书
党的群众路线教育实践活动个人批评与自我批评
2014/10/16 职场文书
民主生活会汇报材料
2014/12/15 职场文书