基于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 Gin实现文件上传下载的示例代码
Apr 02 Golang
为什么不建议在go项目中使用init()
Apr 12 Golang
golang interface判断为空nil的实现代码
Apr 24 Golang
用golang如何替换某个文件中的字符串
Apr 25 Golang
Goland使用Go Modules创建/管理项目的操作
May 06 Golang
Golang 实现获取当前函数名称和文件行号等操作
May 08 Golang
golang内置函数len的小技巧
Jul 25 Golang
详解Golang如何优雅的终止一个服务
Mar 21 Golang
Golang MatrixOne使用介绍和汇编语法
Apr 19 Golang
Golang 结构体数据集合
Apr 22 Golang
Go web入门Go pongo2模板引擎
May 20 Golang
Go语言编译原理之源码调试
Aug 05 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 PDO中文乱码解决办法
2009/07/20 PHP
php获取访问者浏览页面的浏览器类型
2017/01/23 PHP
PHP 类与构造函数解析
2017/02/06 PHP
php简单随机字符串生成方法示例
2017/04/19 PHP
js检测客户端不是firefox则提示下载
2007/04/07 Javascript
根据对象的某一属性进行排序的js代码(如:name,age)
2010/08/10 Javascript
ASP.NET中AJAX 调用实例代码
2012/05/03 Javascript
jQuery中复合属性选择器用法实例
2014/12/31 Javascript
基于jquery实现的自动补全功能
2015/03/12 Javascript
AngularJS2中一种button切换效果的实现方法(二)
2017/03/27 Javascript
微信小程序switch组件使用详解
2018/01/31 Javascript
layui当点击文本框时弹出选择框,显示选择内容的例子
2019/09/02 Javascript
django 自定义用户user模型的三种方法
2014/11/18 Python
Python 功能和特点(新手必学)
2015/12/30 Python
Python实现购物车功能的方法分析
2017/11/10 Python
python批量读取txt文件为DataFrame的方法
2018/04/03 Python
详解pyqt5 动画在QThread线程中无法运行问题
2018/05/05 Python
Python类和对象的定义与实际应用案例分析
2018/12/27 Python
Python魔法方法详解
2019/02/13 Python
详解Python Matplotlib解决绘图X轴值不按数组排序问题
2019/08/05 Python
Python 基于wxpy库实现微信添加好友功能(简洁)
2019/11/29 Python
Python实现病毒仿真器的方法示例(附demo)
2020/02/19 Python
40行Python代码实现天气预报和每日鸡汤推送功能
2020/02/27 Python
Python常用编译器原理及特点解析
2020/03/23 Python
Python3.7在anaconda里面使用IDLE编译器的步骤详解
2020/04/29 Python
Python xlwt模块使用代码实例
2020/06/10 Python
CSS3系列之3D制作方法案例
2017/08/14 HTML / CSS
JNI的定义
2012/11/25 面试题
怀念母亲教学反思
2014/04/28 职场文书
工作作风承诺书
2014/08/30 职场文书
小学趣味运动会加油稿
2014/09/25 职场文书
仙境之桥观后感
2015/06/16 职场文书
工作服管理制度范本
2015/08/06 职场文书
如何利用STAR法则制作留学文书?
2019/08/26 职场文书
Spring实现内置监听器
2021/07/09 Java/Android
【DOTA2】当街暴打?PSG LGD vs VG - DPC 2022 WINTER TOUR CN
2022/04/02 DOTA