最近在项目中踩了一个深坑——“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. MAC iterm2 常用快捷键大全

    标签 新建标签:command + t 关闭标签:command + w 切换标签:command + 数字 / command + 左右方向键 切换全屏:command + enter 查找:com ...

  2. ceph-fuse客户端问题排查流程

    本文讲述了ceph-fuse客户端问题排查基本流程:) 首先查看集群的整体情况 ceph -s 是否有osd挂掉,是否有pg非active ceph-fuse进程是否存在? ps -ef |grep ...

  3. Field部分参数设置含义

    /** * Field.Store.COMPRESS:压缩保存,用于长文本或二进制数据 * Field.Store.YES:保存 * Field.Store.NO:不保存 * * Field.Inde ...

  4. scrapy实战3利用fiddler对手机app进行抓包爬虫图片下载(重写ImagesPipeline):

    关于fiddler的使用方法参考(http://jingyan.baidu.com/article/03b2f78c7b6bb05ea237aed2.html) 本案例爬取斗鱼 app 先利用fidd ...

  5. django基础知识之认识MVT MVC:

    MVT Django是一款python的web开发框架 与MVC有所不同,属于MVT框架 m表示model,负责与数据库交互 v表示view,是核心,负责接收请求.获取数据.返回结果 t表示templ ...

  6. LINUX安装源码软件经典三部曲

    这几天一直在搞suse下的mplyaer.ffmpeg等源码编译安装,总结出源码软件安装三部曲,网上称为经典三部曲. 这三步分别为: 1. ./configure [options] 2. make ...

  7. Spring中@value以及属性注入的学习

    1.简单的Java配置 配置文件(jdbc.properties) jdbc.driverClassName=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://1 ...

  8. 缓存实践Cache Aside Pattern

    Cache Aside Pattern旁路缓存,是对缓存应用的一个总结,包括读数据方案和写数据方案. 读数据方案 先读cache,如果命中则返回 如果miss则读db 将db的数据存入缓存 写数据方案 ...

  9. Java编程思想:泛型方法

    import java.util.*; public class Test { public static void main(String[] args) { // GenericMethods.t ...

  10. 从动态代理到Spring AOP(上)

    一.前言 虽然平时日常开发很少用到动态代理,但是动态代理在底层框架等有着非常重要的意义.比如Spring AOP使用cglib和JDK动态代理,Hibernate底层使用了javassit和cglib ...