问题
小提示, 若想直接查看原理, 可从接口原理开始查看.
有这样一段GO
代码:
func main() {var obj interface{}fmt.Printf("obj == nil. %b\n", obj == nil)type st struct{}var s *stobj = sfmt.Printf("s == nil. %b\n", s == nil)fmt.Printf("obj == nil. %b\n", obj == nil)
}
先盲猜一下结果.
- 第一次
nil
的判断, 结果为true
, 没什么疑问吧 - 第二次判断,
s
为空指针, 结果为true
- 第三次判断,
obj
与s
相等, 故也为空指针, 结果为true
.
如果你也是这么认为, 那么结果会令你像我一样十分惊讶:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I5e4q0VX-1659519198125)(https://oss-blog.cdn.hujingnb.com/img/202208022034502.png)]
???第三次判断, obj
不为nil
???意不意外? 惊不惊喜? 刺不刺激? 为什么会发生这样的事情呢?
搭建 gdb 调试环境
为了知道为什么发生这种问题, 我尝试了各种方式, 断点调试, 查看汇编内容等等, 最终发现, 通过gdb
工具查看十分方便.
在这之前, 先简单介绍一下gdb
调试环境的使用. 不感兴趣可直接跳过
为了方便, 直接使用docker
镜像了. 这里我使用的镜像为: golang:1.18
其他版本大同小异. 这里直接上结论了, 中间踩坑过程不再赘述.
# 安装 gdb 工具
apt update && apt install -y gdb
echo 'add-auto-load-safe-path /usr/local/go/src/runtime/runtime-gdb.py' > /root/.gdbinit
# 编译 go 文件. 关闭所有的优化, 防止调试时与编写的内容不一致
go build -gcflags "all=-N -l" main.go
# 进行调试
gdb ./main
是不是很简单呀.
调试与揭秘
为了方便调试, 我将无关内容去掉, 调试使用的程序如下:
package mainfunc main() {var obj interface{}type st struct{}var s *stprintln(obj == nil)obj = sprintln(obj == nil)
}
我们分别在obj
赋值前后, 打印局部变量:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U0gamwOL-1659519198126)(https://oss-blog.cdn.hujingnb.com/img/202208022106041.png)]
我们惊奇的发现, 在obj
被赋值之前, obj == nil
为 TRUE
, 但是打印变量后发现, obj
并不是一个空指针.
而在obj
赋值之后, obj == nil
为 FALSE
. 前后的差异就在于_type
字段.
在此处, 我有理由得出这样的结论:
golang
中的interface
的实现是一个结构体, 包括_type/data
两个字段- 判断
interface
是否为nil
时, 若两个字段均为nil
, 则interface
为nil
, 否则不为nil
.
同时, 我又好奇的查看了一下obj
的类型:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IrguDZyi-1659519198127)(https://oss-blog.cdn.hujingnb.com/img/202208022108216.png)]
正如上面所看到的, interface
是一个特殊的类型, 其在实现上是一个叫做runtime.eface
的结构体.
解惑. OK, 到这里, 就已经解答了我们最开的时候的疑惑, 在将一个空指针对象赋值给internface
的时候, 会给interface
结构体的字段_type
赋值, 使得_type
字段不为nil
, 进而导致interface
变量不为nil
.
以上, 是我本次问题查找的原因及初步查找的过程. 我基于此对接口的实现原理进行了查阅. 后面就直接进行原理介绍, 不再穿插查找过程了, 否则着实影响观看体验.
接口原理
GO
在存储接口类型的变量时, 根据接口中是否包含方法, 分别存储为不同类型的结构体.
若接口中不包含方法, 将其存储为runtime.eface
. 如:
type TestInter interface {
}
var obj interface{}
var obj2 TestInter
若接口中含有方法, 则将其存储为runtime.iface
. 如:
type TestInter interface {testFunc()
}
var obj2 TestInter
eface
eface
定义在文件runtime2.go
中. 其结构体定义如下:
type eface struct {_type *_type // 保存类型信息data unsafe.Pointer // 保存内容
}type _type struct {size uintptr // 类型大小ptrdata uintptr // 没整明白是干什么用的...hash uint32 // 类型的哈希值. 可用于快速判断类型是否相等tflag tflag // 类型的额外信息align uint8 // 变量的内存对齐大小fieldAlign uint8 kind uint8 // 类型equal func(unsafe.Pointer, unsafe.Pointer) bool // 比较此类型对象是否相等gcdata *byte // 垃圾收集的 GC 数据str nameOffptrToThis typeOff
}// Pointer 就是一个指针
type Pointer *ArbitraryType
type ArbitraryType int
可以看到, 在_type
中基本上已经存储了一个类型的所有信息. (虽然有几个字段还没整明白, 不过对于理解整体逻辑影响不大)
_type
用来对类型进行标识, 想比底层反射的实现也是根据他来的.
iface
iface
区别于eface
的地方, 就是iface
需要额外存储接口的方法信息. 若是一个不含有方法的接口, 是可以接收所有值得. 但带有方法的接口, 则被赋值的内容必须实现了所有的方法. 其结构体定义如下:
type iface struct {tab *itabdata unsafe.Pointer
}type itab struct {inter *interfacetype // 保存接口的信息. 用于确定变量的接口类型_type *_type // data 指向值得类型信息, 上面已经出现过了hash uint32 // 从 _type.hash 拿过来的. 当将 interface 类型变量向下转型时, 用于快速判断. _ [4]byte// 记录接口实现的所有方法. // 若 fun[0]==0, 说明 _type 没有实现此接口. // (没错, 是有可能没实现的. 比如转型失败)// 否则, 说明实现了此接口. 所有方法的函数指针在内存中顺序存放. // fun[0] 记录的是第一个方法的地址// 顺便提一句, 函数按照名称的字段序在内存中存放fun [1]uintptr
}type interfacetype struct {typ _type // 接口类型pkgpath name // 包名mhdr []imethod // 接口定义的方法集
}
现在知道我们在将interface
类型的变量进行转型或类型断言的时候, GO
是如何处理的了吧? 其实接口自己是知道自己的类型的.
另外, 在将一个结构体赋值给interface
的时候, GO
也在其中进行了特定的操作. 可以在runtime.iface.go
文件中, 看到一批以conv
开头的方法, 用来将一个变量转为数据指针unsafe.Pointer
. 在此先按下不表…
总结
以上, 简单的了解了GO
接口的内部实现, 发现接口在实现上和普通的结构体变量十分不同, 其内部是通过一个特定的结构体来记录信息的. 知道了接口的实现, 我们在平常开发时, 碰到接口就应该注意一下, 若interface
判断不为nil
, 存储的值也可能为nil
.
最后, GO1.18
之后增加了泛型的支持, 以前使用interface
接收任意参数的场景 也可以使用泛型替代了.