最近在项目中踩了一个深坑——“Golang中一个包含nil指针的接口不是nil接口”,现象是函数内返回了nil给一个对象,使用interface接收函数返回值判断始终不为nil。总结下分享出来,如果你不是很理解这句话,那推荐认真看下下面的示例代码,避免以后写代码时踩坑。

示例一

先一起来看下这段代码,你感觉有没有问题呢?

type IPeople interface {
hello()
}
type People struct {
} func (p *People) hello() {
fmt.Println("github.com/meetbetter")
} func errFunc1(in int) *People {
if in == 0 {
fmt.Println("importantFunc返回了一个nil")
return nil
} else {
fmt.Println("importantFunc返回了一个非nil值")
return &People{}
} } func main() {
var i IPeople in := 0 i = errFunc1(in) if i == nil { fmt.Println("哈,外部接收到也是nil")
} else { fmt.Println("咦,外部接收到不是nil哦")
fmt.Printf("%v, %T\n", i, i)
} }

这段代码的执行结果是:

importantFunc返回了一个nil
咦,外部接收到不是nil哦
<nil>, *main.People

可以看到在main函数中收到的返回值不是nil, 明明在errFunc1()函数中返回的是nil,到了main函数为什么收到的不是nil呢?

这是因为:将nil赋值给*People后再将*People赋值给interface,*People本身是是个指向nil的指针,但是将其赋给接口时只是接口中的值为nil,但是接口中的类型信息为*main.People而不是nil,所以这个接口不是nil。

是的,Golang中的interface类型包含两部分信息——值信息和类型信息,只有interface的值合并类型都为nil时interface才为nil,interface底层实现可以在后面的源码分析看到。

先来看看正确的处理接口返回值的方法,是直接将nil赋给interface:


func rightFunc(in int) IPeople {
if in == 0 {
fmt.Println("importantFunc返回了一个nil")
return nil
} else {
fmt.Println("importantFunc返回了一个非nil值")
return &People{}
} }

示例二

下面的代码更清晰的证明了一个包含nil指针的接口不是nil接口的结论:

type IPeople interface {
hello()
}
type People struct {
} func (p *People) hello() {
fmt.Println("github.com/meetbetter")
} //错误:将nil的people给空接口后接口就不为nil,因为interface中的value为nil但type不为nil func errFunc() *People { return nil
} //正确处理返回nil给接口的方法,返回时go就确定了接口是不是nil
func rightFunc() IPeople { return nil
}
func main() { var i IPeople
i = errFunc()
if i == nil { //想通过接口是否为nil来判断故障,却始终判断接口非空 fmt.Println("errFunc对了哦,外部接收到也是nil")
fmt.Println(reflect.TypeOf(i))
} else { fmt.Println("errFunc错了咦,外部接收到不是nil哦")
fmt.Println(reflect.TypeOf(i))
} i = rightFunc()
if i == nil { fmt.Println("rightFunc对了哦,外部接收到也是nil")
fmt.Println(reflect.TypeOf(i))
} else { fmt.Println("rightFunc错了咦,外部接收到不是nil哦")
fmt.Println(reflect.TypeOf(i)) } }

输出结果:

errFunc错了咦,外部接收到不是nil哦
*main.People
rightFunc对了哦,外部接收到也是nil
<nil>

interface底层实现

下面的注释信息来自参考文章中,从interface底层实现可以看出iface比eface 中间多了一层itab结构, itab 存储_type信息和[]fun方法集,所以即使data指向了nil 并不代表interface 就是nil, 还要考虑_type信息。

type eface struct {      //空接口
_type *_type //类型信息
data unsafe.Pointer //指向数据的指针(go语言中特殊的指针类型unsafe.Pointer类似于c语言中的void*)
}
type iface struct { //带有方法的接口
tab *itab //存储type信息还有结构实现方法的集合
data unsafe.Pointer //指向数据的指针(go语言中特殊的指针类型unsafe.Pointer类似于c语言中的void*)
}
type _type struct {
size uintptr //类型大小
ptrdata uintptr //前缀持有所有指针的内存大小
hash uint32 //数据hash值
tflag tflag
align uint8 //对齐
fieldalign uint8 //嵌入结构体时的对齐
kind uint8 //kind 有些枚举值kind等于0是无效的
alg *typeAlg //函数指针数组,类型实现的所有方法
gcdata *byte
str nameOff
ptrToThis typeOff
}
type itab struct {
inter *interfacetype //接口类型
_type *_type //结构类型
link *itab
bad int32
inhash int32
fun [1]uintptr //可变大小 方法集合
}

以上完整代码均整理在Github-跟着示例代码学Golang项目

参考文章:

Golang第一大坑

"一个包含nil指针的接口不是nil接口"的讨论

Go“一个包含nil指针的接口不是nil接口”踩坑的更多相关文章

  1. 【记录一个问题】golang神坑,明明返回了接口指针类型的nil值,却无法用if判断

    先看看导致异常的代码: package main import ( "fmt" "log" ) type MyError1 struct{ MyErrorCod ...

  2. 谈谈Delphi中的类和对象1---介绍几个概念 && 对象是一个地地道道的指针

    参考:http://blog.163.com/liang_liu99/blog/static/88415216200952123412180/ 以下的介绍主要针对的是Delphi的面向对象的知识,可能 ...

  3. go语言笔记——切片底层本质是共享数组内存!!!绝对不要用指针指向 slice切片本身已经是一个引用类型就是指针

    切片 切片(slice)是对数组一个连续片段的引用(该数组我们称之为相关数组,通常是匿名的),所以切片是一个引用类型(因此更类似于 C/C++ 中的数组类型,或者 Python 中的 list 类型) ...

  4. jquery[siblings]取得一个包含匹配的元素集合中每一个元素的所有唯一同辈元素的元素集合

    取得一个包含匹配的元素集合中每一个元素的所有唯一同辈元素的元素集合,用于筛选同辈元素的表达式 $("#pageList").click(function(){ $(this).pa ...

  5. C语言里的指针探析——type *name[] 在函数参数里面,是一个二维指针

    type *name[] 在函数参数里面声明和不在函数里面声明其实不一样. type *name[] 如果在函数参数里声明,则name 是一个二维指针,并不是一个指针数组,而如果不在函数参数里声明,则 ...

  6. [leetcode-117]填充每个节点的下一个右侧节点指针 II

    (1 AC) 填充每个节点的下一个右侧节点指针 I是完美二叉树.这个是任意二叉树 给定一个二叉树 struct Node { int val; Node *left; Node *right; Nod ...

  7. 最短路径(给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。 说明:每次只能向下或者向右移动一步。)

    给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小. 说明:每次只能向下或者向右移动一步. 例: 输入: [ [1,3,1], [1,5,1], [ ...

  8. 定义一个包含标签inclusion_tag, 调用模板时报错.. 应该是路径 不对吧...我的templates 是放在app 目录下的.<待处理>

    # 自定义模板标签. 标签的作用,在模板中 实现逻辑,如if ,for 等 from django.template import Library from datetime import datet ...

  9. cocos2d-x getParent() 获得一个父类的一个node型指针,转换为父类类型

    void CenterLayer::zhanzheng(CCObject* pSender){ ((GameScene*)this->getParent())->showLayer(Gam ...

随机推荐

  1. idea上MyBatis第一个例子

    接着上面创建的maven项目来. 1.java目录下创建cn.happy.entity包 2.idea下创建数据库连接 配置连接参数 3.把数据库表变成实体类 导入成功,改一下包名就可以用了 4.新建 ...

  2. Linux命令分类汇总(1~6)

    Linux命令分类汇总 序号 命令 参数 英文释义 功能说明 (一)线上查询及帮助命令(2个) 1 man manual 查看命令帮助,命令的词典,还有info 2 help h 查看Linux内置命 ...

  3. [python] 安装TensorFlow问题 解决Cannot uninstall 'wrapt'. It is a distutils installed project

    cmd安装 pip install tensorflow 1.遇到了 ERROR: Cannot uninstall 'wrapt'. It is a distutils installed proj ...

  4. 机器学习经典算法之EM

    一.简介 EM 的英文是 Expectation Maximization,所以 EM 算法也叫最大期望算法. 我们先看一个简单的场景:假设你炒了一份菜,想要把它平均分到两个碟子里,该怎么分? 很少有 ...

  5. Presto 0.22.0 安装记录

    1. 下载 & 解压 # 下载 wget https://repo1.maven.org/maven2/com/facebook/presto/presto-server/0.220/pres ...

  6. 从零开始基于go-thrift创建一个RPC服务

    Thrift 是一种被广泛使用的 rpc 框架,可以比较灵活的定义数据结构和函数输入输出参数,并且可以跨语言调用.为了保证服务接口的统一性和可维护性,我们需要在最开始就制定一系列规范并严格遵守,降低后 ...

  7. Linux命令学习-mkdir命令

    Linux中,mkdir命令的全称是make directory,即创建目录的意思. 假设当前处于wintest用户的主目录,路径为 /home/wintest ,存在文件夹testA,进入testA ...

  8. Node.js热部署代码,实现修改代码后自动重启服务方便实时调试

    写PHP等脚本语言的时候,已经习惯了修改完代码直接打开浏览器去查看最新的效果.而Node.js 只有在第一次引用时才会去解析脚本文件,以后都会直接访问内存,避免重复载入,这种设计虽然有利于提高性能,却 ...

  9. CDQZ集训DAY10 日记

    又一次跪了,跪在了神奇的数据范围上. T1上来打完暴力之后觉得是数据结构题,像三维偏序,于是开始往各种数据结构上想,主席树,线段树+calc,平衡树,树套树,CDQ……最终在经过一番思考之后选择去打C ...

  10. [NOIP2016]换教室 题解(奇怪的三种状态)

    2558. [NOIP2016]换教室 [题目描述] 对于刚上大学的牛牛来说,他面临的第一个问题是如何根据实际情况申请合适的课程. 在可以选择的课程中,有2n节课程安排在n个时间段上.在第i(1< ...