目录
  • 前言
  • 接口是什么
  • iface 和 eface 结构体
  • _type 是什么
  • itab 是什么
  • 生成的 itab 是怎么被使用的
  • itab 关键方法的实现
  • 根据 interfacetype 和 _type 初始化 itab
  • 接口断言过程总览(类型转换的关键)
  • panicdottypeI 与 panicdottypeE
  • iface 和 eface 里面的 data 是怎么来的
  • convT* 方法
    • Java 里面的小整数享元模式
  • 总结
interfaceinterface
interface接口

前言

interfaceinterfaceinterface
iface.goiface.go

接口是什么

interface

我们先来看看下面的代码:

// main.go
package main

type Flyable interface {
 Fly()
}

// go tool compile -N -S -l main.go
func main() {
 var f1 interface{}
 println(f1) // CALL    runtime.printeface(SB)

 var f2 Flyable
 println(f2) // CALL    runtime.printiface(SB)
}
go tool compile -N -S -l main.gomain.go
// main.go:10 => println(f1)
0x0029 00041 (main.go:10)  CALL  runtime.printeface(SB)
// main.go:13 => println(f2)
0x004f 00079 (main.go:13)  CALL  runtime.printiface(SB)
println(f1)runtime.printefaceprinteface
func printeface(e eface) {
 print("(", e._type, ",", e.data, ")")
}
printefaceefaceinterface{}println(f2)runtime.printiface
func printiface(i iface) {
 print("(", i.tab, ",", i.data, ")")
}
interface{}efaceFlyableiface
ifaceeface
ifaceefaceinterface{}

iface 和 eface 结构体

ifaceefaceruntime/iface.go
// 非空接口(如:io.Reader)
type iface struct {
 tab  *itab          // 方法表
 data unsafe.Pointer // 指向变量本身的指针
}

// 空接口(interface{})
type eface struct {
 _type *_type         // 接口变量的类型
 data  unsafe.Pointer // 指向变量本身的指针
}
_type

比如,我们有下面的代码:

package main

type Bird struct {
 name string
}

func (b Bird) Fly() {
}

type Flyable interface {
 Fly()
}

func main() {
 bird := Bird{name: "b1"}
 var efc interface{} = bird // efc 是 eface
 var ifc Flyable = bird // ifc 是 iface

 println(efc) // runtime.printeface
 println(ifc) // runtime.printiface
}
efcefaceeface_typeBirddata&bird

ifcifaceifacedata&bird

_type 是什么

_type
// _type 是 go 里面所有类型的一个抽象,里面包含 GC、反射、大小等需要的细节,
// 它也决定了 data 如何解释和操作。
// 里面包含了非常多信息:类型的大小、哈希、对齐及 kind 等信息
type _type struct {
    size       uintptr // 数据类型共占用空间的大小
    ptrdata    uintptr // 含有所有指针类型前缀大小
    hash       uint32  // 类型 hash 值;避免在哈希表中计算
    tflag      tflag   // 额外类型信息标志
    align      uint8   // 该类型变量对齐方式
    fieldAlign uint8   // 该类型结构体字段对齐方式
    kind       uint8   // 类型编号
    // 用于比较此类型对象的函数
    equal func(unsafe.Pointer, unsafe.Pointer) bool
    // gc 相关数据
    gcdata    *byte
    str       nameOff // 类型名字的偏移
    ptrToThis typeOff
}
_type
_typeitabinitruntime/iface.go
typ := m._type
x := typ.uncommon() // 结构体类型

nt := int(x.mcount)   // 实际类型的方法数量
// 实际类型的方法数组,数组元素为 method
xmhdr := (*[1 << 16]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt]
_typeuncommonx.moff

我们可以参考一下下图想象一下:

itab 是什么

iface*itabitab
// 编译器已知的 itab 布局
type itab struct {
 inter *interfacetype // 接口类型
 _type *_type
 hash  uint32
 _     [4]byte
 fun   [1]uintptr // 变长数组. fun[0]==0 意味着 _type 没有实现 inter 这个接口
}

// 接口类型
// 对应源代码:type xx interface {}
type interfacetype struct {
    typ     _type     // 类型信息
    pkgpath name      // 包路径
    mhdr    []imethod // 接口的方法列表
}
interfacetype_type
itab*interfacetype*_type
// i 在底层是一个 interfacetype 类型
type i interface {
 A()
 C()
}

// t 底层会用 _type 来表示
// t 里面有 A、B、C、D 方法
// 因为实现了 i 中的所有方法,所以 t 实现了接口 i
type t struct {}
func (t) A()  {}
func (t) B()  {}
func (t) C()  {}
func (t) D()  {}
itab

说明:

itabintertype Reader interface{}iAC_typeiABCDititivar v i = t{}itabtiitabtiitabitmhdritabt

所以,我们有如下结论:

itabinterfacetype_typeitabitabitabitabfun[0] = 0

生成的 itab 是怎么被使用的

itabTableitabitabitabitab
// 表里面缓存了 itab
itabTable     = &itabTableInit
itabTableInit = itabTableType{size: itabInitSize}

// 全局的 itab 表
type itabTableType struct {
    size    uintptr             // entries 的长度,2 的次方
    count   uintptr             // 当前 entries 的数量
    entries [itabInitSize]*itab // 保存 itab 的哈希表
}
itabTableTypeentriesinterfacetype_typeitabinterfacetype_typeitab

具体怎么使用,我们可以看看下面的例子:

package main

type Flyable interface {
 Fly()
}

type Runnable interface {
 Run()
}

var _ Flyable = (*Bird)(nil)
var _ Runnable = (*Bird)(nil)

type Bird struct {
}

func (b Bird) Fly() {
}

func (b Bird) Run() {
}

// GOOS=linux GOARCH=amd64 go tool compile -N -S -l main.go > main.s
func test() {
 // f 的类型是 iface
 var f Flyable = Bird{}
 // Flyable 转 Runnable 本质上是 iface 到 iface 的转换
 f.(Runnable).Run() // CALL runtime.assertI2I(SB)
 // 这个 switch 里面的类型断言本质上也是 iface 到 iface 的转换
 // 但是 switch 里面的类型断言失败不会引发 panic
 switch f.(type) {
 case Flyable: // CALL runtime.assertI2I2(SB)
 case Runnable: // CALL runtime.assertI2I2(SB)
 }
 if _, ok := f.(Runnable); ok { // CALL runtime.assertI2I2(SB)
 }

 // i 的类型是 eface
 var i interface{} = Bird{}
 // i 转 Flyable 本质上是 eface 到 iface 的转换
 i.(Flyable).Fly() // CALL runtime.assertE2I(SB)
 // 这个 switch 里面的类型断言本质上也是 eface 到 iface 的转换
 // 但是 switch 里面的类型断言失败不会引发 panic
 switch i.(type) {
 case Flyable: // CALL runtime.assertE2I2(SB)
 case Runnable: // CALL runtime.assertE2I2(SB)
 }
 if _, ok := i.(Runnable); ok { // CALL runtime.assertE2I2(SB)
 }
}

我们对上面的代码生成伪汇编代码:

GOOS=linux GOARCH=amd64 go tool compile -N -S -l main.go > main.s

main.sruntime.assert*assertI2IassertI2I2assertE2IassertE2I2assertasserttruefalsetrue
2i.(T)switch...case

我们可以看看它们的源码,看看有什么不一样:

// 直接根据 interfacetype/_type 获取 itab
func assertE2I(inter *interfacetype, t *_type) *itab {
 if t == nil {
  // 显式转换需要非nil接口值。
  panic(&TypeAssertionError{nil, nil, &inter.typ, ""})
 }
 // getitab 的第三个参数是 false
 // 表示 getiab 获取不到 itab 的时候需要 panic
 return getitab(inter, t, false)
}

// 将 eface 转换为 iface
// 因为 e 包含了 *_type
func assertE2I2(inter *interfacetype, e eface) (r iface) {
 t := e._type
 if t == nil {
  return
 }
 // getitab 的第三个参数是 true
 // 表示 getitab 获取不到 itab 的时候不需要 panic
 tab := getitab(inter, t, true)
 if tab == nil {
  return
 }
 r.tab = tab
 r.data = e.data
 return
}
getitab
22getitabchanselectchanselectchan
assertE2I2switch...casev, ok := i.(T)i.(T)iTpanic

对于 go 中的接口断言可以总结如下:

assertI2IifaceifacepanicassertI2I2ifaceifacepanicassertE2IefaceifacepanicassertE2I2efaceifacepanicassertI2IE2EIifaceEeface2v, ok := i.(T)switch x.(type) ... case2i.(T)

当然,这里说的转换不是说直接转换,只是说,在转换的过程中会用到 assert* 方法。

assertI2IassertI2I2interfacetype_typeitabitab

同时,我们也应该注意到,上面的转换都是转换到 iface 而没有转换到 eface 的操作,这是因为,所有类型都可以转换为空接口(interface{},也就是 eface)。根本就不需要断言。

上面的内容可以结合下图理解一下:

itab 关键方法的实现

itab

itab
itabTableTypeitabgetitabinter_typeentriesitabitabitabAdditabitabTableTypeentriesentriesinter_type
itab
getitabinterfacetype_typeitabitabAdditabitab
getitabcanfailswitch...casev, ok := i.(T)
// 获取某一个类型的 itab(从 itabTable 中查找,键是 inter 和 _type 的哈希值)
// 查找 interfacetype + _type 对应的 itab
// 找不到就新增。
func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
 if len(inter.mhdr) == 0 {
  throw("internal error - misuse of itab")
 }

 // 不包含 Uncommon 信息的类型直接报错
 if typ.tflag&tflagUncommon == 0 {
  if canfail {
   return nil
  }
  name := inter.typ.nameOff(inter.mhdr[0].name)
  panic(&TypeAssertionError{nil, typ, &inter.typ, name.name()})
 }

 // 保存返回的 itab
 var m *itab

 // t 指向了 itabTable(全局的 itab 表)
 t := (*itabTableType)(atomic.Loadp(unsafe.Pointer(&itabTable)))
 // 会先从全局 itab 表中查找,找到就直接返回
 if m = t.find(inter, typ); m != nil {
  goto finish
 }

 // 没有找到,获取锁,再次查找。
 // 找到则返回
 lock(&itabLock)
 if m = itabTable.find(inter, typ); m != nil {
  unlock(&itabLock)
  goto finish
 }

 // 没有在缓存中找到,新建一个 itab
 m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*goarch.PtrSize, 0, &memstats.other_sys))
 // itab 的
 m.inter = inter
 m._type = typ
 m.hash = 0
 // itab 初始化
 m.init()
 // 将新创建的 itab 加入到全局的 itabTable 中
 itabAdd(m)
 // 释放锁
 unlock(&itabLock)
finish:
 // == 0 表示没有任何方法
 // 下面 != 0 表示有 inter 和 typ 有方法的交集
 if m.fun[0] != 0 {
  return m
 }
 // 用在 switch x.(type) 中的时候,允许失败而不是直接 panic
 // 但在 x.(Flyable).Fly() 这种场景会直接 panic
 if canfail {
  return nil
 }

 // 没有找到有方法的交集,panic
 panic(&TypeAssertionError{concrete: typ, asserted: &inter.typ, missingMethod: m.init()})
}
itabAdditabitabitabTable
itabAdd75%
// 必须保持 itabLock。
func itabAdd(m *itab) {
 // 正在分配内存的时候调用的话报错
 if getg().m.mallocing != 0 {
  throw("malloc deadlock")
 }

 t := itabTable
 // 容量已经超过 75% 的负载了,hash 表扩容
 if t.count >= 3*(t.size/4) {
  // 75% load factor(实际上是:t.size *0.75)
  // 扩展哈希表。原来 2 倍大小。
  // 我们撒谎告诉 malloc 我们需要无指针内存,因为所有指向的值都不在堆中。
  // 2 是 size 和 count 这两个字段需要的空间
  t2 := (*itabTableType)(mallocgc((2+2*t.size)*goarch.PtrSize, nil, true))
  t2.size = t.size * 2

  // 复制条目。
  // 注意:在复制时,其他线程可能会查找itab,但找不到它。
  // 没关系,然后它们会尝试获取itab锁,因此等待复制完成。
  iterate_itabs(t2.add)    // 遍历旧的 hash 表,复制函数指针到 t2 中
  if t2.count != t.count { // 复制出错
   throw("mismatched count during itab table copy")
  }

  // 发布新哈希表。使用原子写入:请参见 getitab 中的注释。
  // 使用 t2 覆盖 itabTable
  atomicstorep(unsafe.Pointer(&itabTable), unsafe.Pointer(t2))
  // 使用新的 hash 表
  // 因为 t 是局部变量,指向旧的地址,
  // 但是扩容之后是新的地址了,所以现在需要将新的地址赋给 t
  t = itabTable
  // 注:旧的哈希表可以在此处进行GC。
 }
 // 将 itab 加入到全局哈希表
 t.add(m)
}
itabAdditabitabTable75%itabTable

根据 interfacetype 和 _type 初始化 itab

itabinterfacetype_typeitab
itabinit
// init 用 m.inter/m._type 对的所有代码指针填充 m.fun 数组。
// 如果该类型不实现接口,它将 m.fun[0] 设置为 0 ,并返回缺少的接口函数的名称。
// 可以在同一个m上多次调用,甚至同时调用。
func (m *itab) init() string {
 inter := m.inter    // 接口
 typ := m._type      // 实际的类型
 x := typ.uncommon()

 // inter 和 typ 都具有按名称排序的方法,并且接口名称是唯一的,因此可以在锁定步骤中迭代这两个;
 // 循环时间复杂度是 O(ni+nt),不是 O(ni*nt)
 ni := len(inter.mhdr) // 接口的方法数量
 nt := int(x.mcount)   // 实际类型的方法数量
 // 实际类型的方法数组,数组元素为 method
 xmhdr := (*[1 << 16]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt] // 大小无关紧要,因为下面的指针访问不会超出范围
 j := 0
 // 用来保存 inter/_type 对方法列表的数组,数组元素为 unsafe.Pointer(是实际类型方法的指针)
 methods := (*[1 << 16]unsafe.Pointer)(unsafe.Pointer(&m.fun[0]))[:ni:ni] // 保存 itab 方法的数组
 // 第一个方法的指针
 var fun0 unsafe.Pointer
imethods:
 for k := 0; k < ni; k++ { // 接口方法遍历
  i := &inter.mhdr[k]                // i 是接口方法, imethod 类型
  itype := inter.typ.typeOff(i.ityp) // 接口的方法类型
  name := inter.typ.nameOff(i.name)  // 接口的方法名称
  iname := name.name()               // 接口的方法名
  ipkg := name.pkgPath()             // 接口的包路径
  if ipkg == "" {
   ipkg = inter.pkgpath.name()
  }

  // 根据接口方法查找实际类型的方法
  for ; j < nt; j++ { // 实际类型的方法遍历
   t := &xmhdr[j]               // t 是实际类型的方法,method 类型
   tname := typ.nameOff(t.name) // 实际类型的方法名
   // 比较接口的方法跟实际类型的方法是否一致
   if typ.typeOff(t.mtyp) == itype && tname.name() == iname {
    // 实际类型的包路径
    pkgPath := tname.pkgPath()
    if pkgPath == "" {
     pkgPath = typ.nameOff(x.pkgpath).name()
    }

    // 如果是导出的方法
    // 则保存到 itab 中
    if tname.isExported() || pkgPath == ipkg {
     if m != nil {
      ifn := typ.textOff(t.ifn) // 实际类型的方法指针(通过这个指针可以调用实际类型的方法)
      if k == 0 {
       // 第一个方法
       fun0 = ifn // we'll set m.fun[0] at the end
      } else {
       methods[k] = ifn
      }
     }
     // 比较下一个方法
     continue imethods
    }
   }
  }
  // 没有实现接口(实际类型没有实现 interface 中的任何一个方法)
  m.fun[0] = 0
  return iname // 返回缺失的方法名,返回值在类型断言失败的时候会需要提示用户
 }
 // 实现了接口
 m.fun[0] = uintptr(fun0)
 return ""
}

接口断言过程总览(类型转换的关键)

runtime.assert*
ifaceifaceifaceefaceefaceiface
interfacetype_typeitab

上面的内容可能有点混乱,让人摸不着头脑,但是我们通过上面的讲述,相信已经了解了 go 接口中底层的一些实现细节,现在,就让我们重新来捋一下,看看 go 接口到底是怎么实现的:

switch...casev, ok := i.(T)i.(T).xx()panic

接着,我们就可以通过下图来了解 go 里面的接口整体的实现原理了(还是以上面的代码作为例子):

var f Flyable = Bird{}

ifaceitabFlyable
ifaceiface
f.(Runnable)_, ok := f.(Runnable)switch f.(type)caseRunnable

FlyableRunnableifaceifaceifaceinterfacetype
var i interface{} = Bird{}
efaceefaceBird

efaceiface
i.(Flyable)_, ok := i.(Runnable)switch i.(type)caseFlyable
_typeBirddataBird

panicdottypeI 与 panicdottypeE

ifaceruntime.assert*
package main

type Flyable interface {
 Fly()
}

type Cat struct {
}

func (c Cat) Fly() {
}

func (c Cat) test() {
}

// GOOS=linux GOARCH=amd64 go tool compile -N -S -l main.go > main.s
func main() {
 var b interface{}
 var _ = b.(int) // CALL runtime.panicdottypeE(SB)

 var c Flyable = &Cat{}
 c.(Cat).test() // CALL runtime.panicdottypeI(SB)
}
b.(int)nilint*CatCatruntime.panicdottypeEruntime.panicdottypeI
// 在执行 e.(T) 转换时如果转换失败,则调用 panicdottypeE
// have:我们的动态类型。
// want:我们试图转换为的静态类型。
// iface:我们正在转换的静态类型。
// 转换的过程:尝试将 iface 的 have 转换为 want 失败了。
// 不是调用方法的时候的失败。
func panicdottypeE(have, want, iface *_type) {
 panic(&TypeAssertionError{iface, have, want, ""})
}

// 当执行 i.(T) 转换并且转换失败时,调用 panicdottypeI
// 跟 panicdottypeE 参数相同,但是 hava 是动态的 itab 类型
func panicdottypeI(have *itab, want, iface *_type) {
 var t *_type
 if have != nil {
  t = have._type
 }
 panicdottypeE(t, want, iface)
}
panic

iface 和 eface 里面的 data 是怎么来的

我们先看看下面的代码:

package main

type Bird struct {
}

func (b Bird) Fly() {
}

type Flyable interface {
 Fly()
}

// GOOS=linux GOARCH=amd64 go tool compile -N -S -l main.go > main.s
func main() {
 bird := Bird{}
 var efc interface{} = bird // CALL runtime.convT(SB)
 var ifc Flyable = bird     // CALL runtime.convT(SB)
 println(efc, ifc)
}
convT

convT* 方法

ifaceconv*conv*ifacedata
// convT 将 v 指向的 t 类型的值转换为可以用作接口值的第二个字的指针(接口的第二个字是指向 data 的指针)。
// data(Pointer) => 指向 interface 第 2 个字的 Pointer
func convT(t *_type, v unsafe.Pointer) unsafe.Pointer {
 // ... 其他代码
 // 分配 _type 类型所需要的内存
 x := mallocgc(t.size, t, true)
 // 将 v 指向的值复制到刚刚分配的内存上
 typedmemmove(t, x, v)
 return x
}

我们发现,在这个过程,实际上是将值复制了一份:

iface.godata
// 转换 uint16 类型值为 interface 里面 data 的指针。
// 如果是 0~255 的整数,返回指向 staticuint64s 数组里面对应下标的指针。
// 否则,分配新的内存地址。
func convT16(val uint16) (x unsafe.Pointer) {
 // 如果小于 256,则使用共享的内存地址
 if val < uint16(len(staticuint64s)) {
  x = unsafe.Pointer(&staticuint64s[val])
  if goarch.BigEndian {
   x = add(x, 6)
  }
 } else {
  // 否则,分配新的内存
  x = mallocgc(2, uint16Type, false)
  *(*uint16)(x) = val
 }
 return
}
ifaceeface
staticuint64s0~255

convT256staticuint64sconvT160~255staticuint64s[]

这实际上是享元模式。

Java 里面的小整数享元模式

staticuint64s

我们来看看 Java 中的例子:

class Test {
    public static void main(String[] args) {
        Integer k1 = 127;
        Integer k2 = 127;
        System.out.println(k1 == k2); // true
        System.out.println(k1.equals(k2)); // true

        Integer k10 = 128;
        Integer k20 = 128;
        System.out.println(k10 == k20); // false
        System.out.println(k10.equals(k20)); // true
    }
}
-128~1270~255
==Integer-128~127truefalse-128~127
java.lang.Integer.IntegerCache

总结

interfaceifaceefaceifacetype i interface{}efaceinterface{}/anyifaceitabdataeface_typedataitabitabitabitabitabitabinterfacetype_typeitab_typei.(T)ifaceifaceefaceifacepanicswitch i.(type) { case ...}ifaceefaceifacepanicitabitab75%2staticuint64s