基于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缓冲channel和非缓冲channel的区别说明
Apr 25 Golang
解决golang在import自己的包报错的问题
Apr 29 Golang
Golang: 内建容器的用法
May 05 Golang
golang gopm get -g -v 无法获取第三方库的解决方案
May 05 Golang
Go语言实现Snowflake雪花算法
Jun 08 Golang
Golang生成Excel文档的方法步骤
Jun 09 Golang
Go语言并发编程 sync.Once
Oct 16 Golang
golang生成并解析JSON
Apr 14 Golang
Golang 遍历二叉树
Apr 19 Golang
Golang 对es的操作实例
Apr 20 Golang
Go语言入门exec的基本使用
May 20 Golang
Go语言测试库testify使用学习
Jul 23 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
实时抓取YAHOO股票报价的代码
2006/10/09 PHP
深入PHP magic quotes的详解
2013/06/17 PHP
yii2.0之GridView自定义按钮和链接用法
2014/12/15 PHP
php面向对象与面向过程两种方法给图片添加文字水印
2015/08/26 PHP
PHP面向对象程序设计高级特性详解(接口,继承,抽象类,析构,克隆等)
2016/12/02 PHP
PHP模型Model类封装数据库操作示例
2019/03/14 PHP
js 判断一个元素是否在页面中存在
2012/12/27 Javascript
Javascript闭包用法实例分析
2015/01/23 Javascript
javascript函数式编程实例分析
2015/04/25 Javascript
JavaScript面对国际化编程时的一些建议
2015/06/24 Javascript
js时间戳转为日期格式的方法
2015/12/28 Javascript
AngularJS监听路由变化的方法
2017/03/07 Javascript
react以create-react-app为基础创建项目
2018/03/14 Javascript
微信小程序云开发之模拟后台增删改查
2019/05/16 Javascript
package.json中homepage属性的作用详解
2020/03/11 Javascript
python提取字典key列表的方法
2015/07/11 Python
python3.0 模拟用户登录,三次错误锁定的实例
2017/11/02 Python
从头学Python之编写可执行的.py文件
2017/11/28 Python
Python批量提取PDF文件中文本的脚本
2018/03/14 Python
Python cookbook(数据结构与算法)从字典中提取子集的方法示例
2018/03/22 Python
Python爬虫之正则表达式基本用法实例分析
2018/08/08 Python
Python matplotlib画图与中文设置操作实例分析
2019/04/23 Python
python将字符串转换成json的方法小结
2019/07/09 Python
使用Python项目生成所有依赖包的清单方式
2020/07/13 Python
html5中如何将图片的绝对路径转换成文件对象
2018/01/11 HTML / CSS
css 如何让背景图片拉伸填充避免重复显示
2013/07/11 HTML / CSS
野兽派官方旗舰店:THE BEAST 野兽派
2016/08/05 全球购物
Bench加拿大官方网站:英国城市服装品牌
2017/11/03 全球购物
澳大利亚家用电器在线商店:Billy Guyatts
2020/05/05 全球购物
Nike瑞士官网:Nike CH
2021/01/18 全球购物
医科大学毕业生自荐信
2014/02/03 职场文书
班子群众路线教育实践个人对照检查材料思想汇报
2014/09/30 职场文书
铁路安全反思材料
2014/12/24 职场文书
2015年医务人员医德医风自我评价
2015/03/03 职场文书
音乐课《小猫钓鱼》教学反思
2016/02/18 职场文书
MySQL 视图(View)原理解析
2021/05/19 MySQL