Golang 接口原理

问题

小提示, 若想直接查看原理, 可从接口原理开始查看.

有这样一段GO代码:

func main() {
    var obj interface{}
    fmt.Printf("obj == nil. %b\n", obj == nil)
    type st struct{}
    var s *st
    obj = s
    fmt.Printf("s == nil. %b\n", s == nil)
    fmt.Printf("obj == nil. %b\n", obj == nil)
}

先盲猜一下结果.

  1. 第一次nil的判断, 结果为 true, 没什么疑问吧
  2. 第二次判断, s为空指针, 结果为true
  3. 第三次判断, objs相等, 故也为空指针, 结果为true.

如果你也是这么认为, 那么结果会令你像我一样十分惊讶:

image-20220802203427661

???第三次判断, 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 main

func main() {
    var obj interface{}
    type st struct{}
    var s *st
    println(obj == nil)
    obj = s
    println(obj == nil)
}

我们分别在obj赋值前后, 打印局部变量:

image-20220802210607987

image-20220802210623591

我们惊奇的发现, 在obj被赋值之前, obj == nilTRUE, 但是打印变量后发现, obj并不是一个空指针.

而在obj赋值之后, obj == nilFALSE. 前后的差异就在于_type字段.

在此处, 我有理由得出这样的结论:

  • golang中的interface的实现是一个结构体, 包括_type/data两个字段
  • 判断interface是否为nil时, 若两个字段均为nil, 则interfacenil, 否则不为nil.

同时, 我又好奇的查看了一下obj的类型:

image-20220802210829177

正如上面所看到的, 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       nameOff
    ptrToThis typeOff
}

// Pointer 就是一个指针
type Pointer *ArbitraryType
type ArbitraryType int

可以看到, 在_type中基本上已经存储了一个类型的所有信息. (虽然有几个字段还没整明白, 不过对于理解整体逻辑影响不大)

_type用来对类型进行标识, 想比底层反射的实现也是根据他来的.

iface

iface区别于eface的地方, 就是iface需要额外存储接口的方法信息. 若是一个不含有方法的接口, 是可以接收所有值得. 但带有方法的接口, 则被赋值的内容必须实现了所有的方法. 其结构体定义如下:

type iface struct {
    tab  *itab
    data 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接收任意参数的场景 也可以使用泛型替代了.

订阅评论
提醒
guest
0 评论
内联反馈
查看所有评论
0
希望看到您的想法,请发表评论。x