Dig101: dig more, simplified more and know more

今天我们聊聊万物皆可为的接口(interface)底层设计。

interface 被定义为一组方法的签名。

有了它,我们可以订立方法契约,去抽象和约束实现。

而 Go 的基础类型,可以认为是没有实现任何方法的空 interface,也就是万物皆为的 interface。

(Go 语言没有泛型,接口可以作为一种替代实现)

接口也被寄予厚望,主力开发 Russ Cox 曾说过:

从语言设计的角度来看,Go 的接口是静态的,在编译时检查过的,在需要时是动态的。如果我可以将 Go 的一个特性导出到其他语言中,那就是接口。Go Data Structures: Interfaces[1]

那到底 interface 是怎么设计的底层结构呢?

又怎么支持的duck typing[2]

在类型断言时又发生了什么?

带着这些问题,我们往下看

文章目录

  • 0x01 底层结构一样么

    • eface

    • iface

  • 0x02 类型如何相互转换

    • convXXX 的命名

    • 起初的 convT2{I,E} 和 convI2I

    • 针对类型优化后的 convXXX

  • 0x03 类型断言如何实现

    • 查表是否匹配

    • 尝试插入更新

    • 动态判定效率优化

0x01 底层结构一样么

我们知道定义接口有这两种方式,那他们底层结构是一样的么?

// 方式1var a interface{}// 方式2type Stringer interface {  String() string}var b Stringer

答案是【不一样】

我们用 gdb 打印下对应类型(gdb 相关见 Tips-如何优雅的使用GDB调试Go)

// 空接口类型// 有函数定义的接口类型// itable相关类型

以此可见 Go 内部定义了两种 interface(但都是两个机器字)

eface

空接口,指没有定义方法的接口

内部存储了构造类型(concrete type)typedata

eface

iface

有方法的接口

有了相比efacetype更丰富的itab字段,其中记录了构造类型及所实现的 interface 类型的类型和方法

iface

0x02 类型如何相互转换

如下代码,当我们做接口赋值时,Go 又会怎样填充底层结构呢?

type Binary uint64func (i Binary) String() string {return strconv.Itoa(int(i))}

func conversion() {var b Stringervar i Binary = 1  b = i // <= 这里发生了什么println(b.String())}

gdb 进到 b = i 这一步,会发现他调用了runtime/iface.go:convT64方法实现 iface 的赋值

查阅源码,会发现很多convXXX函数, 他们是干什么的?

convXXX 的命名

convFrom2To 指代 To=From 的转换

From 和 To 的类型有三种:(参见cmd/compile/internal/types/type.go:Tie)

  • E (eface)
  • I (iface)
  • T (Type)

这一堆函数看的人眼晕,但参照提交specialize convT2x, don't alloc for zero vals[3]深入分析,就会清晰许多

起初的 convT2{I,E} 和 convI2I

最初只有 convT2{I,E} 和 convI2I

主要实现分配内存(newobject),然后拷贝赋值(typedmemmove)

convI2I 还会有getitab, 具体是什么我们后边类型断言时说

然后也在调用他们前(walkexpr)做了优化

  • 减少值拷贝

ToType 为类指针(pointer-shaped)或者一个机器字内(int)的话,可以直接存入 interface 的 data 字段(主要优化在这里)

pointer-shaped类型: ptr, chan, map, func, unsafe.Pointer

再辅以 type 的存储,就只是两个字(two-word)的拷贝

  • 减少内存分配

零值,bool/byte 可以不用分配内存,而用已存在值(zerobase,staticbytes)

只读的全局变量(readonly global)直接可以用

1kb 以内,不escape到堆上,非interface的变量可以使用栈上分配的临时变量(stack temporary initialized)

这类 value 最后以取地址形式转化为 interface:{type/itab, &value}.

  • interface 转空接口(eface)

可以丢弃除type以外的itab

tmp = i.itabif tmp != nil {  tmp = tmp.type}e = iface{tmp, i.data}

针对类型优化后的 convXXX

但这里会有一些可以优化的点,如:

  • 分配内存是否可以需要清零?

类指针的类型需要清零,不然内存可能有脏数据

但无指针类型(pointer-free)如拷贝时直接可以覆盖对应内存则不需要

int其拷贝在一个机器字内完成,不需要分配时清零 (32 位系统上不调用convT64,就可以保证访问内存是安全的原子操作)

  • 是否可以简化值拷贝?

int,string,slice这些 Type 分配的x拷贝val时,可以简化为 *(*Type)(x) = val

  • 拷贝内存是否可以不增加 gc 调用(写屏障)?

按 ToType 类型是否含指针区分 类指针类型(pointer-shaped): convT2{E,I} 需要拷贝时 gc 调用(typedmemmove)

无指针类型(pointer-free): convT2{E,I}noptr 不需要拷贝时 gc 调用(memmove)

这样一看就明白这些函数的用意了,还是为了针对性的提高转化效率

最后结合其调用处convXXX列表如下:

// cmd/compile/internal/gc/walk.go:walkexprcase OCONVIFACE:  ...  fnname, needsaddr := convFuncName(fromType, toType)
fnname fromType needsaddr
convI2I iface
convT{16,
32,64}
整型数据
(无指针,
机器字内)
convTstring string
convTslice slice
convT2E Type
convT2Enoptr 无指针
Type
convT2I Type
convT2Inoptr 无指针
Type

不会存在 convE2E 和 convE2I

needsaddr: 类型不含指针,大小大于 64 位字或未知大小时,使用值的地址来存

0x03 类型断言如何实现

interface 支持类型断言,来动态判断其构造类型,

判定成功可返回对应构造类型,便于调用其方法

可构造类型实现 interface 不需要显示声明,

那如下代码是怎么确定 interface b(构造类型是Binary)实现Stringer呢?

type Binary uint64

func (i Binary) String() string {return fmt.Sprint(i)}

func typeAssert() {var b interface{} = Binary(1)  v, ok := b.(Stringer)println(v, ok)}

调试后会发现,其调用了assertE2I2

这里函数命名有两类,如下

assertE2I: v := eface1.(iface1)

assertE2I2: v,ok := eface1.(iface1)

这里有一点,类型断言非 v,ok 方式的,断言失败会 panic)

原来其内部进行了itab表(itabTable)查询 interface 和构造类型的映射表,如果匹配则说明实现

下边代码分析如下

首先初始 512 个 entry 的表

const itabInitSize = 512type itabTableType struct {// 上限  size    uintptr// 当前用量  count   uintptr  entries [itabInitSize]*itab}

查表是否匹配

在类型断言中调用 getitab(inter, typ, canfail) 查表

  • 先不加锁 atomic 读取 itabTable,找到返回
  • 未找到加锁再查一遍,找到返回
  • 还没有就创建一个 itab 添加到表中,添加完后解锁
  • 期间如果判定不匹配则按是否可以 panic(canfail)返回

其中查表用到 itabTable.find(inter, typ)

插入用到 itabAdd(m)

尝试插入更新

  • 插入前需先用m.inter/m._type pair 初始化 m.fun 数组,不匹配则m.fun[0]==0

(m.fun 类型 [1]uintptr,实际指向是大小为接口定义方法数的方法数组。详见 func (m *itab) init())

  • 用量 count 超过上限的 75%触发扩容,大小为 2 倍以上(要向上内存对齐),扩容后更新 itabTable 是原子操作

  • 以 itab m 的 interface 类型和构造类型的 hash 计算对应 itabTable 的起始偏移,然后插入到其后第一个不为空的 entry。如果已存在则直接返回

这里用到了开放地址探测法,公式是:

h(i) = h0 + i*(i+1)/2 mod 2^k

具体插入用到 itabTable.add(m)

这里和其实 map 插入的逻辑很相似

动态判定效率优化

不过,这里有一个问题?

假定,interface 定义了ni个方法,构造类型实现nt个方法,

常规匹配构造类型是否实现全部ni个方法需要两层遍历,复杂度为O(ni*nt)

这样在初始化itab.fun或类型断言匹配时效率会比较低。

Go 设计时也考虑了这个问题,把复杂度降低为O(ni+nt)

这也是使用 hashtable 的原因之一:

  • 首先 interface 的函数定义列表itab.inter.mhdr和构造类型的函数列表itab.fun都是按函数名排好序的

  • 这样第一次 itab 初始化时,判定构造类型是否实现函数列表可以O(ni+nt)内遍历完成

  • 然后用开放地址探测法更新到 itabtable 中,查询时也可以用同样的方式定位到此 itab 是否存在。

两个(有序)列表的遍历匹配代码精简如下:

// runtime/iface.go:init()j:=0imethods:// 遍历interface定义函数列表for k := 0; k < ni; k++ {// 遍历构造类型函数列表for ; j < nt; j++ {// 如果两者类型(type),包路径(pkgpath),函数名(name)匹配if xxx {// 将方法记录到fun0(最终全匹配则赋值给 m.fun)continue imethods      }    }// 未全匹配    m.fun[0] = 0  }  m.fun[0] = uintptr(fun0)

总结一下 interface 的底层设计:

  • interface 分为空接口(eface)和接口(iface)两类,但都是两机器字(two-word)存储结构
  • interface 转换中针对不同类型做了优化,主要集中于提升内存分配和值拷贝效率
  • interface 类型断言时动态判定,利用有序列表遍历+全局哈希表表缓存优化判定效率

See More:官方解释 InterfaceSlice[4] 为什么不能直接转化


最后留个问题:

下边这段转换代码内部没有调convT64,为什么?

var b Stringer = Binary(1)_ = b.String()

这个问题下一篇文章再来给出解答。

本文代码见 NewbMiao/Dig101-Go[5]

参考资料

[1]

Go Data Structures: Interfaces: https://research.swtch.com/interfaces

[2]

duck typing: https://en.wikipedia.org/wiki/Duck_typing

[3]

specialize convT2x, don't alloc for zero vals: https://go-review.googlesource.com/c/go/+/36476

[4]

InterfaceSlice: https://github.com/golang/go/wiki/InterfaceSlice

[5]

NewbMiao/Dig101-Go: https://github.com/NewbMiao/Dig101-Go/blob/master/types/interface/interface.go


推荐阅读

  • Dig101: Go之for-range排坑指南
  • Dig101: Go之灵活的slice
  • Dig101: Go之string那些事
  • Dig101:Go之读懂map的底层设计
  • Dig101:Go之聊聊struct的内存对齐
  • Tips-如何优雅的使用GDB调试Go

原创不易,如果有用,欢迎转发、点个在看分享给更多人。

微信内外链不能跳转,戳阅读原文查看原文中参考资料


欢迎关注我,一位爱折腾的开发(奶爸):热衷搬砖、价投。

android 集成同一interface不同泛型_Dig101:Go之读懂interface的底层设计相关推荐

  1. 【Android】Android 集成商米内置打印机打印票据

    文章目录 [Android]Android 集成商米内置打印机打印票据 1.集成商米打印依赖 2.规范接口接口 3.使用到的相关对象以及工具类 4.MainActivity初始化接口 5.Uniapp ...

  2. Android集成腾讯X5浏览器内核库

    Android集成腾讯X5浏览器内核库 一.相关配置 1. 相关地址 2.引入SDK 3. AndroidManifest配置 二.Application中初始化内核 三.代码实现 1. 自定义带Pr ...

  3. Android 集成微信支付和支付宝支付工具类

    Android 集成微信支付和支付宝支付工具类 1.前言 去年年底接了一个商城app 外包项目,里面尼涉及到 微信和支付宝支付,这里我整理出几个工具类,下面就和大家分享一下,废话不多说,下面我一步一步 ...

  4. 【Android】Android 集成佳博80打印机打印票据

    文章目录 [Android]Android 集成佳博80打印机打印票据 1.集成佳博80打印机依赖 2.规范调用接口 3.使用到的相关对象以及工具类 4.MainActivity初始化接口 5.Uni ...

  5. 【Android】Android 集成商米钱箱

    文章目录 [Android]Android 集成商米钱箱 1.集成商米打印依赖 2.规范调用接口 3.MainActivity初始化接口 4.Uniapp调用方法 技术分享区 [Android]And ...

  6. 使用IntelliJ IDEA 13搭建Android集成开发环境(图文教程)

    ​[声明] 欢迎转载,但请保留文章原始出处→_→ 生命壹号:http://www.cnblogs.com/smyhvae/ 文章来源:http://www.cnblogs.com/smyhvae/p/ ...

  7. 使用Android Studio搭建Android集成开发环境

    一.Android Studio简单介绍 2013年GoogleI/O大会首次发布了Android Studio IDE(Android平台集成开发环境).它基于Intellij IDEA开发环境,旨 ...

  8. 使用Android Studio搭建Android集成开发环境(图文教程)

    ​[声明] 欢迎转载,但请保留文章原始出处→_→ 生命壹号:http://www.cnblogs.com/smyhvae/ 文章来源:http://www.cnblogs.com/smyhvae/p/ ...

  9. Golang 为什么不能直接将任意类型数组赋值给 []interface{}完成泛型操作

    想用 []interface{} 类型来接受一个任意类型的数组,从而进行泛型操作时,发现直接赋值会发生错误,导致 panic var dataSlice []int = foo() var inter ...

最新文章

  1. 《VMware vCAT权威指南:成功构建云环境的核心技术和方法》一3.6 vCloud计量
  2. sql增删改查_快速搞定数据库增删改查|附思维导图
  3. 投资学习网课笔记(part6)--基金第六课
  4. 创建Live Rates Plan时Sales Organization无法自动带出来的问题
  5. 三目运算符_Java中的三目运算符
  6. 爬虫cookie过期_python instagram 爬虫
  7. response.setContentType()的作用及MIME参数详解
  8. 擦窗机器人不用时怎么收纳_解放双手,再也不用手动擦窗啦
  9. Dropbox推荐使用
  10. SN1SLD16 华为SDH全新原包装2xSTM-16光接口板
  11. java二级线程_计算机二级辅导:Java线程新特征(原子量)
  12. 《信任的速度》读书笔记
  13. Python实现二维码生成器
  14. 中国科学院 导师推荐 计算机,中国科学院计算技术研究所硕士生导师霍志刚
  15. 记模拟器出现横竖屏切换闪屏问题
  16. Dataframe两个表格合并
  17. 离散数学 | 数理逻辑
  18. 电源高性能和平衡区别 文件服务器,win10电源高性能和平衡区别具体有哪些细节...
  19. 如何在 Linux 中使用 Calibre 将 PDF 文件转换为 EPUB 格式?
  20. GCC 编译 C 语言文件

热门文章

  1. 电子元器件首饰!送给你喜欢的女孩!
  2. 科普 | Wi-Fi 6 十问十答
  3. 这位电子工程师,你不能错过。
  4. 《c语言从入门到精通》看书笔记——第3章 数据类型
  5. dict取值_Python基础数据类型「list、tuple、dict」
  6. 手机桌面隐藏大师_应用加密,教你一招隐藏手机桌面上的软件!
  7. mysql 5.6 分区_Mysql5.6—分区表及独享表空间
  8. mac 10.10 apache php,在Mac上10分钟搞定Apache服务器配置
  9. [AT2567] [arc074_c] RGB Sequence
  10. 数组实用类:Arrays