基于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 相关文章推荐
为什么不建议在go项目中使用init()
Apr 12 Golang
golang正则之命名分组方式
Apr 25 Golang
Golang 正则匹配效率详解
Apr 25 Golang
golang通过递归遍历生成树状结构的操作
Apr 28 Golang
Go标准容器之Ring的使用说明
May 05 Golang
golang switch语句的灵活写法介绍
May 06 Golang
Golang 实现获取当前函数名称和文件行号等操作
May 08 Golang
简单聊聊Golang中defer预计算参数
Mar 25 Golang
victoriaMetrics库布隆过滤器初始化及使用详解
Apr 05 Golang
Golang jwt身份认证
Apr 20 Golang
GO中sync包自由控制并发示例详解
Aug 05 Golang
Go gorilla/sessions库安装使用
Aug 14 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读取PDF内容配合Xpdf的使用
2012/11/24 PHP
PHP之短标签开启设置
2013/06/17 PHP
php eval函数一句话木马代码
2015/05/21 PHP
thinkPHP模板引擎用法示例
2016/12/08 PHP
php记录搜索引擎爬行记录的实现代码
2018/03/02 PHP
用JavaScript脚本实现Web页面信息交互
2006/10/11 Javascript
[原创]提供复制本站内容时出现,该文章转自脚本之家等字样的js代码
2007/03/27 Javascript
用javascript为页面添加天气显示实现思路及代码
2013/12/02 Javascript
分享使用AngularJS创建应用的5个框架
2015/12/05 Javascript
JS跨域交互(jQuery+php)之jsonp使用心得
2016/07/01 Javascript
JS监听微信、支付宝等移动app及浏览器的返回、后退、上一页按钮的事件方法
2016/08/05 Javascript
浅析JavaScript中var that=this
2017/02/17 Javascript
easy ui datagrid 从编辑框中获取值的方法
2017/02/22 Javascript
AngularJs1.x自定义指令独立作用域的函数传入参数方法
2018/10/09 Javascript
微信小程序学习笔记之文件上传、下载操作图文详解
2019/03/29 Javascript
vue模式history下在iis中配置流程
2019/04/17 Javascript
vue+element tabs选项卡分页效果
2020/06/29 Javascript
JS实现星星海特效
2019/12/24 Javascript
python 循环遍历字典元素的简单方法
2016/09/11 Python
Python File(文件) 方法整理
2019/02/18 Python
python使用Matplotlib改变坐标轴的默认位置
2019/10/18 Python
Python控制台输出时刷新当前行内容而不是输出新行的实现
2020/02/21 Python
巧用CSS3的calc()宽度计算做响应模式布局的方法
2018/03/22 HTML / CSS
HTML5 3D旋转相册的实现示例
2019/12/03 HTML / CSS
欧舒丹美国官网:L’Occitane美国
2018/02/23 全球购物
顶丰TOPPIK台湾官网:增发纤维假发,告别秃发困扰
2018/06/13 全球购物
院药学专业个人求职信
2013/09/21 职场文书
建筑毕业生自我鉴定
2013/10/18 职场文书
业务经理岗位职责
2013/11/11 职场文书
大二法学专业职业生涯规划范文
2014/02/12 职场文书
爱心捐助倡议书
2014/05/19 职场文书
2015年实习生工作总结报告
2015/04/28 职场文书
pyqt5打包成exe可执行文件的方法
2021/05/14 Python
世界十大儿童漫画书排名,法国国宝漫画排第五,第二是轰动日本连环
2022/03/18 欧美动漫
微信小程序APP的事件绑定以及传递参数时的冒泡和捕获
2022/04/19 Javascript
Nginx本地配置SSL访问的实例教程
2022/05/30 Servers