源网址:http://legendtkl.com/2017/07/01/golang-interface-implement/

0.引言

在上一篇文章 《深入理解 Go Interface》中从设计和使用的角度介绍了 Golang 的 interface,作为补充,这篇文章将从源码级别来看看 interface 的具体实现。所有源码分析都是基于 Go 1.8.3。在开始之前,我们先看一个例子。

  1. func Foo(x interface{}) {
  2. if x == nil {
  3. fmt.Println("empty interface")
  4. return
  5. }
  6. fmt.Println("non-empty interface")
  7. }
  8.  
  9. func main() {
  10. var x *int = nil
  11. Foo(x)
  12. }

如果你对于上面的输出结果有疑惑,那么不妨带着疑问来看这篇文章。上面的例子的输出结果如下

  1. $ go run test_interface.go
  2. non-empty interface

1. interface 底层结构

根据 interface 是否包含有 method,底层实现上用两种 struct 来表示:iface 和 eface。eface表示不含 method 的 interface 结构,或者叫 empty interface。对于 Golang 中的大部分数据类型都可以抽象出来 _type 结构,同时针对不同的类型还会有一些其他信息。

iface 表示 non-empty interface 的底层实现。相比于 empty interface,non-empty 要包含一些 method。method 的具体实现存放在 itab.fun 变量里。如果 interface 包含多个 method,这里只有一个 fun 变量怎么存呢?这个下面再细说。

  1. type eface struct {
  2. _type *_type
  3. data unsafe.Pointer
  4. }
  5.  
  6. type _type struct {
  7. size uintptr // type size
  8. ptrdata uintptr // size of memory prefix holding all pointers
  9. hash uint32 // hash of type; avoids computation in hash tables
  10. tflag tflag // extra type information flags
  11. align uint8 // alignment of variable with this type
  12. fieldalign uint8 // alignment of struct field with this type
  13. kind uint8 // enumeration for C
  14. alg *typeAlg // algorithm table
  15. gcdata *byte // garbage collection data
  16. str nameOff // string form
  17. ptrToThis typeOff // type for pointer to this type, may be zero
  18. }
  1. type iface struct {
  2. tab *itab
  3. data unsafe.Pointer
  4. }
  5.  
  6. // layout of Itab known to compilers
  7. // allocated in non-garbage-collected memory
  8. // Needs to be in sync with
  9. // ../cmd/compile/internal/gc/reflect.go:/^func.dumptypestructs.
  10. type itab struct {
  11. inter *interfacetype
  12. _type *_type
  13. link *itab
  14. bad int32
  15. inhash int32 // has this itab been added to hash?
  16. fun []uintptr // variable sized
  17. }

我们使用实际程序来看一下。

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. )
  6.  
  7. type MyInterface interface {
  8. Print()
  9. }
  10.  
  11. type MyStruct struct{}
  12. func (ms MyStruct) Print() {}
  13.  
  14. func main() {
  15. x :=
  16. var y interface{} = x
  17. var s MyStruct
  18. var t MyInterface = s
  19. fmt.Println(y, z)
  20. }

查看汇编代码。

  1. $ go build -gcflags '-l' -o interface11 interface11.go
  2. $ go tool objdump -s "main\.main" interface11
  3. TEXT main.main(SB) /Users/kltao/code/go/examples/interface11.go
  4. interface11.go: 0x10870f0 65488b0c25a0080000 GS MOVQ GS:0x8a0, CX
  5. interface11.go: 0x10870f9 483b6110 CMPQ 0x10(CX), SP
  6. interface11.go: 0x10870fd 0f86de000000 JBE 0x10871e1
  7. interface11.go: 0x1087103 4883ec70 SUBQ $0x70, SP
  8. interface11.go: 0x1087107 48896c2468 MOVQ BP, 0x68(SP)
  9. interface11.go: 0x108710c 488d6c2468 LEAQ 0x68(SP), BP
  10. interface11.go: 0x1087111 48c744243001000000 MOVQ $0x1, 0x30(SP)
  11. interface11.go: 0x108711a 488d057fde0000 LEAQ 0xde7f(IP), AX
  12. interface11.go: 0x1087121 MOVQ AX, (SP)
  13. interface11.go: 0x1087125 488d442430 LEAQ 0x30(SP), AX
  14. interface11.go: 0x108712a MOVQ AX, 0x8(SP)
  15. interface11.go: 0x108712f e87c45f8ff CALL runtime.convT2E(SB)
  16. interface11.go: 0x1087134 488b442410 MOVQ 0x10(SP), AX
  17. interface11.go: 0x1087139 MOVQ AX, 0x38(SP)
  18. interface11.go: 0x108713e 488b4c2418 MOVQ 0x18(SP), CX
  19. interface11.go: 0x1087143 48894c2440 MOVQ CX, 0x40(SP)
  20. interface11.go: 0x1087148 488d15b1000800 LEAQ 0x800b1(IP), DX
  21. interface11.go: 0x108714f MOVQ DX, (SP)
  22. interface11.go: 0x1087153 488d542430 LEAQ 0x30(SP), DX
  23. interface11.go: 0x1087158 MOVQ DX, 0x8(SP)
  24. interface11.go: 0x108715d e8fe45f8ff CALL runtime.convT2I(SB)

代码 17 行 var y interface{} = x 调用了函数 runtime.convT2E ,将 int 类型的 x 转换成 empty interface。代码 19 行 var t MyInterface = s 将 MyStruct 类型转换成 non-empty interface: MyInterface。

  1. func convT2E(t *_type, elem unsafe.Pointer) (e eface) {
  2. ...
  3.  
  4. x := newobject(t)
  5. typedmemmove(t, x, elem)
  6. e._type = t
  7. e.data = x
  8. return
  9. }
  10.  
  11. func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
  12. t := tab._type
  13.  
  14. ...
  15.  
  16. x := newobject(t)
  17. typedmemmove(t, x, elem)
  18. i.tab = tab
  19. i.data = x
  20. return
  21. }

看上面的函数原型,可以看出中间过程编译器将根据我们的转换目标类型的 empty interface 还是 non-empty interface,来对原数据类型进行转换(转换成 <*_type, unsafe.Pointer> 或者 <*itab, unsafe.Pointer>)。这里对于 struct 满不满足 interface 的类型要求(也就是 struct 是否实现了 interface 的所有 method),是由编译器来检测的。

2. itab

iface 结构中最重要的是 itab 结构。itab 可以理解为 pair\ 。当然 itab 里面还包含一些其他信息,比如 interface 里面包含的 method 的具体实现。下面细说。itab 的结构如下。

  1. type itab struct {
  2. inter *interfacetype
  3. _type *_type
  4. link *itab
  5. bad int32
  6. inhash int32 // has this itab been added to hash?
  7. fun []uintptr // variable sized
  8. }

其中 interfacetype 包含了一些关于 interface 本身的信息,比如 package path,包含的 method。上面提到的 iface 和 eface 是数据类型(built-in 和 type-define)转换成 interface 之后的实体的 struct 结构,而这里的 interfacetype 是我们定义 interface 时候的一种抽象表示。

  1. type interfacetype struct {
  2. typ _type
  3. pkgpath name
  4. mhdr []imethod
  5. }
  6.  
  7. type imethod struct { //这里的 method 只是一种函数声明的抽象,比如 func Print() error
  8. name nameOff
  9. ityp typeOff
  10. }

_type 表示 concrete type。fun 表示的 interface 里面的 method 的具体实现。比如 interface type 包含了 method A, B,则通过 fun 就可以找到这两个 method 的具体实现。这里有个问题 fun 是长度为 1 的 uintptr 数组,那么怎么表示多个 method 呢?看一下测试程序。

  1. package main
  2.  
  3. type MyInterface interface {
  4. Print()
  5. Hello()
  6. World()
  7. AWK()
  8. }
  9.  
  10. func Foo(me MyInterface) {
  11. me.Print()
  12. me.Hello()
  13. me.World()
  14. me.AWK()
  15. }
  16.  
  17. type MyStruct struct {}
  18.  
  19. func (me MyStruct) Print() {}
  20. func (me MyStruct) Hello() {}
  21. func (me MyStruct) World() {}
  22. func (me MyStruct) AWK() {}
  23.  
  24. func main() {
  25. var me MyStruct
  26. Foo(me)
  27. }

看一下函数调用对应的汇编代码。

  1. $ go build -gcflags '-l' -o interface8 interface8.go
  2. $ go tool objdump -s "main\.Foo" interface8
  3. TEXT main.Foo(SB) /Users/kltao/code/go/examples/interface8.go
  4. interface8.go: 0x104c060 65488b0c25a0080000 GS MOVQ GS:0x8a0, CX
  5. interface8.go: 0x104c069 483b6110 CMPQ 0x10(CX), SP
  6. interface8.go: 0x104c06d JBE 0x104c0d7
  7. interface8.go: 0x104c06f 4883ec10 SUBQ $0x10, SP
  8. interface8.go: 0x104c073 48896c2408 MOVQ BP, 0x8(SP)
  9. interface8.go: 0x104c078 488d6c2408 LEAQ 0x8(SP), BP
  10. interface8.go: 0x104c07d 488b442418 MOVQ 0x18(SP), AX
  11. interface8.go: 0x104c082 488b4830 MOVQ 0x30(AX), CX //取得 Print 函数地址
  12. interface8.go: 0x104c086 488b542420 MOVQ 0x20(SP), DX
  13. interface8.go: 0x104c08b MOVQ DX, (SP)
  14. interface8.go: 0x104c08f ffd1 CALL CX // 调用 Print()
  15. interface8.go: 0x104c091 488b442418 MOVQ 0x18(SP), AX
  16. interface8.go: 0x104c096 488b4828 MOVQ 0x28(AX), CX //取得 Hello 函数地址
  17. interface8.go: 0x104c09a 488b542420 MOVQ 0x20(SP), DX
  18. interface8.go: 0x104c09f MOVQ DX, (SP)
  19. interface8.go: 0x104c0a3 ffd1 CALL CX //调用 Hello()
  20. interface8.go: 0x104c0a5 488b442418 MOVQ 0x18(SP), AX
  21. interface8.go: 0x104c0aa 488b4838 MOVQ 0x38(AX), CX //取得 World 函数地址
  22. interface8.go: 0x104c0ae 488b542420 MOVQ 0x20(SP), DX
  23. interface8.go: 0x104c0b3 MOVQ DX, (SP)
  24. interface8.go: 0x104c0b7 ffd1 CALL CX //调用 World()
  25. interface8.go: 0x104c0b9 488b442418 MOVQ 0x18(SP), AX
  26. interface8.go: 0x104c0be 488b4020 MOVQ 0x20(AX), AX //取得 AWK 函数地址
  27. interface8.go: 0x104c0c2 488b4c2420 MOVQ 0x20(SP), CX
  28. interface8.go: 0x104c0c7 48890c24 MOVQ CX, (SP)
  29. interface8.go: 0x104c0cb ffd0 CALL AX //调用 AWK()
  30. interface8.go: 0x104c0cd 488b6c2408 MOVQ 0x8(SP), BP
  31. interface8.go: 0x104c0d2 4883c410 ADDQ $0x10, SP
  32. interface8.go: 0x104c0d6 c3 RET
  33. interface8.go: 0x104c0d7 e8f48bffff CALL runtime.morestack_noctxt(SB)
  34. interface8.go: 0x104c0dc eb82 JMP main.Foo(SB)

其中 0x18(SP) 对应的 itab 的值。fun 在 x86-64 机器上对应 itab 内的地址偏移为 8+8+8+4+4 = 32 = 0x20,也就是 0x20(AX) 对应的 fun 的值,此时存放的 AWK 函数地址。然后 0x28(AX) = &Hello,0x30(AX) = &Print,0x38(AX) = &World。对的,每次函数是按字典序排序存放的。

我们再来看一下函数地址究竟是怎么写入的?首先 Golang 中的 uintptr 一般用来存放指针的值,这里对应的就是函数指针的值(也就是函数的调用地址)。但是这里的 fun 是一个长度为 1 的 uintptr 数组。我们看一下 runtime 包的 additab 函数。

  1. func additab(m *itab, locked, canfail bool) {
  2. ...
  3. *(*unsafe.Pointer)(add(unsafe.Pointer(&m.fun[]), uintptr(k)*sys.PtrSize)) = ifn
  4. ...
  5. }

上面的代码的意思是在 fun[0] 的地址后面依次写入其他 method 对应的函数指针。熟悉 C++ 的同学可以类比 C++ 的虚函数表指针来看。

剩下的还有 bad,link,inhash。其中 bad 是一个表征 itab 状态的变量。而这里的 link 是 *itab 类型,是不是表示 interface 的嵌套呢?并不是,interface 的嵌套也是把 method 平铺而已。link 要和 inhash 一起来说。在 runtime 包里面有一个 hash 表,通过 hash[hashitab(interface_type, concrete_type)] 可以取得 itab,这是出于性能方面的考虑。主要代码如下,这里就不再赘述了。

  1. const (
  2. hashSize =
  3. )
  4.  
  5. var (
  6. ifaceLock mutex // lock for accessing hash
  7. hash [hashSize]*itab
  8. )
  9.  
  10. func itabhash(inter *interfacetype, typ *_type) uint32 {
  11. // compiler has provided some good hash codes for us.
  12. h := inter.typ.hash
  13. h += * typ.hash
  14. // TODO(rsc): h += 23 * x.mhash ?
  15. return h % hashSize
  16. }
  17.  
  18. func additab(...) {
  19. ...
  20. h := itabhash(inter, typ)
  21. m.link = hash[h]
  22. m.inhash =
  23. atomicstorep(unsafe.Pointer(&hash[h]), unsafe.Pointer(m))
  24. }

3. Type Assertion

我们知道使用 interface type assertion (中文一般叫断言) 的时候需要注意,不然很容易引入 panic。

  1. func do(v interface{}) {
  2. n := v.(int) // might panic
  3. }
  4.  
  5. func do(v interface{}) {
  6. n, ok := v.(int)
  7. if !ok {
  8. // 断言失败处理
  9. }
  10. }

这个过程体现在下面的几个函数上。

  1. // The assertXXX functions may fail (either panicking or returning false,
  2. // depending on whether they are 1-result or 2-result).
  3. func assertI2I(inter *interfacetype, i iface) (r iface) {
  4. tab := i.tab
  5. if tab == nil {
  6. // explicit conversions require non-nil interface value.
  7. panic(&TypeAssertionError{"", "", inter.typ.string(), ""})
  8. }
  9. if tab.inter == inter {
  10. r.tab = tab
  11. r.data = i.data
  12. return
  13. }
  14. r.tab = getitab(inter, tab._type, false)
  15. r.data = i.data
  16. return
  17. }
  18. func assertI2I2(inter *interfacetype, i iface) (r iface, b bool) {
  19. tab := i.tab
  20. if tab == nil {
  21. return
  22. }
  23. if tab.inter != inter {
  24. tab = getitab(inter, tab._type, true)
  25. if tab == nil {
  26. return
  27. }
  28. }
  29. r.tab = tab
  30. r.data = i.data
  31. b = true
  32. return
  33. }
  34.  
  35. // 类似
  36. func assertE2I(inter *interfacetype, e eface) (r iface)
  37. func assertE2I2(inter *interfacetype, e eface) (r iface, b bool)

4. 总结

从某种意义上来说,Golang 的 interface 也是一种多态的体现。对比其他支持多态特性的语言,实现还是略有差异,很难说谁好谁坏。

5. 参考

  1. Go Data Structure: Interfaces (注:09 年的文章)
  2. A Quick Guide to Go’s Assembler
  3. runtime: need a better itab table #20505

【转】Go Interface 源码剖析的更多相关文章

  1. SpringMVC源码剖析(二)- DispatcherServlet的前世今生

    上一篇文章<SpringMVC源码剖析(一)- 从抽象和接口说起>中,我介绍了一次典型的SpringMVC请求处理过程中,相继粉墨登场的各种核心类和接口.我刻意忽略了源码中的处理细节,只列 ...

  2. 玩转Android之Picasso使用详详详详详详解,从入门到源码剖析!!!!

    Picasso是Squareup公司出的一款图片加载框架,能够解决我们在Android开发中加载图片时遇到的诸多问题,比如OOM,图片错位等,问题主要集中在加载图片列表时,因为单张图片加载谁都会写.如 ...

  3. 老李推荐:第6章6节《MonkeyRunner源码剖析》Monkey原理分析-事件源-事件源概览-命令队列

    老李推荐:第6章6节<MonkeyRunner源码剖析>Monkey原理分析-事件源-事件源概览-命令队列   事件源在获得字串命令并把它翻译成对应的MonkeyEvent事件后,会把这些 ...

  4. 老李推荐:第5章5节《MonkeyRunner源码剖析》Monkey原理分析-启动运行: 获取系统服务引用

    老李推荐:第5章5节<MonkeyRunner源码剖析>Monkey原理分析-启动运行: 获取系统服务引用   上一节我们描述了monkey的命令处理入口函数run是如何调用optionP ...

  5. 豌豆夹Redis解决方案Codis源码剖析:Dashboard

    豌豆夹Redis解决方案Codis源码剖析:Dashboard 1.不只是Dashboard 虽然名字叫Dashboard,但它在Codis中的作用却不可小觑.它不仅仅是Dashboard管理页面,更 ...

  6. 豌豆夹Redis解决方案Codis源码剖析:Proxy代理

    豌豆夹Redis解决方案Codis源码剖析:Proxy代理 1.预备知识 1.1 Codis Codis就不详细说了,摘抄一下GitHub上的一些项目描述: Codis is a proxy base ...

  7. 【安卓网络请求开源框架Volley源码解析系列】定制自己的Request请求及Volley框架源码剖析

    通过前面的学习我们已经掌握了Volley的基本用法,没看过的建议大家先去阅读我的博文[安卓网络请求开源框架Volley源码解析系列]初识Volley及其基本用法.如StringRequest用来请求一 ...

  8. Spring源码剖析依赖注入实现

    Spring源码剖析——依赖注入实现原理 2016年08月06日 09:35:00 阅读数:31760 标签: spring源码bean依赖注入 更多 个人分类: Java   版权声明:本文为博主原 ...

  9. 转 Spring源码剖析——核心IOC容器原理

    Spring源码剖析——核心IOC容器原理 2016年08月05日 15:06:16 阅读数:8312 标签: spring源码ioc编程bean 更多 个人分类: Java https://blog ...

随机推荐

  1. js扩展运算符(spread)是三个点(...)

    作用:将一个数组转为用逗号分隔的参数序列. //该运算符主要用于函数调用.function push(array, ...items) { array.push(...items); } functi ...

  2. ContentProvider工作原理

    --摘自<android插件化开发指南> 1.系统管理类app,比如手机助手,有机会频繁使用ContentProvider 2.通讯录或者短信数据,是以ContentProvider的形式 ...

  3. HDU 5178 pairs【二分】||【尺取】

    <题目链接> 题目大意: 给定一个整数序列,求出绝对值小于等于k的有序对个数. 解题分析: $O(nlong(n))$的二分很好写,这里就不解释了.本题尺取$O(n)$也能做,并且效率很不 ...

  4. codeforces 1058D.Vasya and Triangle (gcd)

    <题目链接> <转载于  >>> > 题目大意: 给出n.m.k.求一个三角形使它的面积等于n*m/k  并且这个三角形的三个顶点所在的坐标为整数点,且顶点满 ...

  5. 002.Oracle安装部署-ASM

    一 环境准备 图形界面:略 安装包: linux.x64_11gR2_database_1of2.zip linux.x64_11gR2_database_2of2.zip 二 安装ASM-Oracl ...

  6. Spring Boot 静态资源访问原理解析

    一.前言 springboot配置静态资源方式是多种多样,接下来我会介绍其中几种方式,并解析一下其中的原理. 二.使用properties属性进行配置 应该说 spring.mvc.static-pa ...

  7. [洛谷P1886]滑动窗口 (单调队列)(线段树)

    ---恢复内容开始--- 这是很好的一道题 题目描述: 现在有一堆数字共N个数字(N<=10^6),以及一个大小为k的窗口. 现在这个从左边开始向右滑动,每次滑动一个单位,求出每次滑动后窗口中的 ...

  8. 二叉搜索树的java实现

    转载请注明出处 一.概念 二叉搜索树也成二叉排序树,它有这么一个特点,某个节点,若其有两个子节点,则一定满足,左子节点值一定小于该节点值,右子节点值一定大于该节点值,对于非基本类型的比较,可以实现Co ...

  9. No compiler is provided in this environment.Perhaps you are running on a JRE rather than a JDK?报错解决

    Maven install 时出现如上错误. Eclipse-->Window-->preferences-->Java-->Installed JREs 查看jdk: 发现此 ...

  10. STM32——TIM2定时器定时

    STM32 中一共有11 个定时器,其中2 个高级控制定时器,4 个普通定时器和2 个基本定时器,以及2 个看门狗定时器和1 个系统嘀嗒定时器.其中系统嘀嗒定时器是前文中所描述的SysTick,看门狗 ...