最近在项目中踩了一个深坑 --"Golang 中一个包含 nil 指针的接口不是 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 {
- var p *People
- return p
- }
- // 正确处理返回 nil 给接口的方式: 直接将 nil 赋给 interface
- func rightFunc() IPeople {
- var p *People
- return p
- }
- func main() {
- if errFunc() == nil {
- fmt.Println("对了哦, 外部接收到也是 nil")
- } else {
- fmt.Println("错了咦, 外部接收到不是 nil 哦")
- }
- if rightFunc() == nil {
- fmt.Println("对了哦, 外部接收到也是 nil")
- } else {
- fmt.Println("错了咦, 外部接收到不是 nil 哦")
- }
- }
输出结果:
对了哦, 外部接收到也是 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 第一大坑 https://studygolang.com/articles/10635
"一个包含 nil 指针的接口不是 nil 接口" 的讨论 https://gocn.vip/question/1011
来源: https://www.cnblogs.com/CodeWithTxT/p/11297300.html