• 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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    先盲猜一下结果.

    1. 第一次nil的判断, 结果为 true, 没什么疑问吧
    2. 第二次判断, s为空指针, 结果为true
    3. 第三次判断, objs相等, 故也为空指针, 结果为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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    是不是很简单呀.

    调试与揭秘

    为了方便调试, 我将无关内容去掉, 调试使用的程序如下:

    package main
    
    func main() {
    	var obj interface{}
    	type st struct{}
    	var s *st
    	println(obj == nil)
    	obj = s
    	println(obj == nil)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

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

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U0gamwOL-1659519198126)(https://oss-blog.cdn.hujingnb.com/img/202208022106041.png)]

    image-20220802210623591

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

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

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

    • golang中的interface的实现是一个结构体, 包括_type/data两个字段
    • 判断interface是否为nil时, 若两个字段均为nil, 则interfacenil, 否则不为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
    
    • 1
    • 2
    • 3
    • 4

    若接口中含有方法, 则将其存储为runtime.iface. 如:

    type TestInter interface {
      testFunc()
    }
    var obj2 TestInter
    
    • 1
    • 2
    • 3
    • 4

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    可以看到, 在_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 // 接口定义的方法集
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    现在知道我们在将interface类型的变量进行转型或类型断言的时候, GO是如何处理的了吧? 其实接口自己是知道自己的类型的.

    另外, 在将一个结构体赋值给interface的时候, GO也在其中进行了特定的操作. 可以在runtime.iface.go文件中, 看到一批以conv开头的方法, 用来将一个变量转为数据指针unsafe.Pointer. 在此先按下不表…

    总结

    以上, 简单的了解了GO接口的内部实现, 发现接口在实现上和普通的结构体变量十分不同, 其内部是通过一个特定的结构体来记录信息的. 知道了接口的实现, 我们在平常开发时, 碰到接口就应该注意一下, 若interface判断不为nil, 存储的值也可能为nil.

    最后, GO1.18之后增加了泛型的支持, 以前使用interface接收任意参数的场景 也可以使用泛型替代了.

  • 相关阅读:
    【TUM公开数据集RGBD-Benchmark工具evaluate_ate.py参数用法原理解读】
    Kafka KRaft线上集群部署实战(broker、controller分离部署)
    【Ubuntu】Windows远程Ubuntu系统
    Kubernetes——部署应用到集群中
    计算机视觉代码学习
    当中国走进全球化的“深水区”,亚马逊云科技解码云时代的中国式跃升
    2011年03月16日 Go生态洞察:Go朝着更高稳定性迈进
    2023下半年软考高级信息系统项目管理师考后解析
    验证码自定义控件
    在进行自动化测试,遇到验证码的问题,怎么办?
  • 原文地址:https://blog.csdn.net/qq_31725391/article/details/126145464