当前位置: 首页 > article >正文

【十四】Golang 接口

💢欢迎来到张胤尘的开源技术站
💥开源如江河,汇聚众志成。代码似星辰,照亮行征程。开源精神长,传承永不忘。携手共前行,未来更辉煌💥

文章目录

  • 接口
    • 接口定义
    • 接口初始化
    • 接口嵌套
    • 空接口
      • 存储任意类型的数据
      • 作为函数参数
      • 类型断言和类型切换
        • 类型断言
        • 类型切换
    • 源码解析
      • 空接口
        • 类型描述符
        • 输出接口值
      • 非空接口
        • `ITab`
        • `itabTableType`
          • 初始化 `itab` 表
          • 添加 `itab`
          • 查找 `itab`
      • 常见操作原理
        • 多态性
          • 接口赋值
          • 方法调用
          • 代码示例
        • 类型断言
          • `TypeAssert`
          • `TypeAssertCache`
          • `TypeAssertCacheEntry`
          • `buildTypeAssertCache`
        • 类型切换
          • `InterfaceSwitch`
          • `InterfaceSwitchCache`
          • `InterfaceSwitchCacheEntry`
          • `buildInterfaceSwitchCache`

接口

接口是一种抽象的数据类型,它定义了一组方法或行为的规范,但不提供具体实现。在 golang 中接口的核心作用是:解耦合多态

  • 解耦合:减少模块之间的直接依赖关系,使得模块之间的修改不会相互影响。另外,通过接口调用者只需要知道接口的规范,而不需要关心具体的实现细节。实现者可以随时替换,只要满足接口规范即可。

  • 多态:多态是指同一个接口可以被不同的实现类使用,调用者在运行时可以根据具体实现调用相应的方法。接口允许不同的实现类提供不同的行为,但调用者可以通过统一的接口进行操作。

接口定义

golang 中,接口通过 type 关键字和 interface 关键字定义。语法如下所示:

type InterfaceName interface {
    Method1(paramType1) returnType1
    Method2(paramType2) returnType2
    ...
}

例如:

type Shape interface {
    Area() float64
    Perimeter() float64
}

在上面代码中,Shape 是一个接口,它定义了两个方法:Area()Perimeter()只要是任何实现了这两个方法的类型都自动实现了 Shape 接口

下面有一个 Circle 类,实现了 Area()Perimeter() 两个方法,如下所示:

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}

从代码中可知 Circle 类型实现了 Shape 接口中的 Area()Perimeter() 方法,它自动满足了 Shape 接口的要求。

接口初始化

接口的初始化并不是直接初始化接口本身,而是通过初始化实现了接口的类型来间接完成的。如果接口初始化时并没有指定具体的实现类,则接口默认是零值 nil,表示它没有指向任何具体的类型或值。如果接口变量为 nil,调用其方法会导致运行时错误。如下所示:

package main

type Shape interface {
	Area() float64
	Perimeter() float64
}

func main() {
	var s Shape = nil
	s.Area() // invalid memory address or nil pointer dereference
}

则需要为其指定正确的实现类来完成初始化行为,如下所示:

package main

import (
	"fmt"
	"math"
)

type Shape interface {
	Area() float64
	Perimeter() float64
}

type Circle struct {
	Radius float64
}

func (c Circle) Area() float64 {
	return math.Pi * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
	return 2 * math.Pi * c.Radius
}

func main() {
	var shape Shape
	shape = Circle{Radius: 2} // 初始化
	fmt.Println(shape.Area()) // 12.566370614359172
}

上述代码中,shape 是一个 Shape 接口变量。通过将 Circle 类型的实例赋值给它,接口变量就初始化完成了,接下来通过 shape 调用了 Area 方法来打印出一个半径为 2 的 Circle 的面积。

接口嵌套

接口的嵌套主要体现在接口之间的继承关系,即一个接口可以嵌套另一个接口。这种嵌套关系可以让接口继承其他接口的方法集合,从而实现更灵活的接口设计。

定义一个接口 Reader ,在接口中定义了一个 Read 方法,用于从某个数据源读取数据到一个字节切片 p 中。如下所示:

type Reader interface {
    Read(p []byte) (n int, err error)
}

定义一个接口 Writer ,在接口中定义了一个 Write 方法,用于将字节切片 p 中的数据写入某个目标。如下所示:

type Writer interface {
    Write(p []byte) (n int, err error)
}

定义一个接口 ReadWriter,通过嵌套 ReaderWriter,它继承了这两个接口的所有方法。如下所示:

type ReadWriter interface {
    Reader
    Writer
}

由于 ReadWriter 继承了 ReaderWriter 两个接口,所以实现 ReadWriter 接口也同样需要实现这两个接口中的方法,如下所示:

type MyReadWriter struct{}

func (m *MyReadWriter) Read(p []byte) (n int, err error) {
	fmt.Printf("MyReadWriter Read~~ %c\n", p)
	return 0, nil
}

func (m *MyReadWriter) Write(p []byte) (n int, err error) {
	fmt.Printf("MyReadWriter Write~~ %c\n", p)
	return 0, nil
}

最后通过 main 函数进行测试,如下所示:

func main() {
	var rw ReadWriter
	rw = &MyReadWriter{}

	rw.Read([]byte("hello world~ ycz"))  // MyReadWriter Read~~ [h e l l o   w o r l d ~   y c z]
	rw.Write([]byte("hello world~ ycz")) // MyReadWriter Write~~ [h e l l o   w o r l d ~   y c z]
}

通过接口嵌套,可以将接口的功能进行模块化设计,从而实现功能的组合和扩展,也可以避免在每个接口中重复定义相同的方法。

空接口

空接口是一种特殊的接口类型,它不包含任何方法。由于它没有任何方法约束,因此可以存储任何类型的值。定义如下所示:

var v interface{}

上述代码中的 v 是一个空接口变量,它可以存储任何类型的值。

在实际开发过程中,会经常使用到空接口,例如:存储任意类型的数据作为函数参数类型断言和类型切换

存储任意类型的数据

空接口可以存储任何类型的值,包括基本类型(如 intstring)、自定义类型(如结构体、切片、映射等)。如下所示:

package main

import "fmt"

func main() {
	var v interface{}

	v = 42
	fmt.Println(v) // 42

	v = "zyc"
	fmt.Println(v) // zyc

	v = []int{1, 2, 3}
	fmt.Println(v) // [1 2 3]
}

作为函数参数

空接口常用于函数参数,使函数能够接受任何类型的值。如下所示:

package main

import "fmt"

func PrintValue(v interface{}) {
	fmt.Println(v)
}

func main() {
	PrintValue(10)             // 10
	PrintValue("zzzzzzz")      // zzzzzzz
	PrintValue([]int{1, 2, 3}) // [1 2 3]
}

类型断言和类型切换

由于空接口存储的值类型不明确,通常需要通过 类型断言类型切换 来获取其实际类型。

类型断言

用于从接口变量中提取具体的类型值。基本语法格式如下所示:

value, ok := interfaceVariable.(Type)value := iface.(Type)

在使用过程中,如果断言失败,ok 会返回 false,而 value 会是目标类型的零值。但是需要注意的是,如果直接使用类型断言而不检查 ok 值可能导致运行时 panic。例如,如果接口变量的实际类型与断言类型不匹配,程序会崩溃。

package main

import "fmt"

func main() {
	var v interface{} = "Hello, World!"

	if str, ok := v.(string); ok {
		fmt.Println("v is string:", str) // v is string: Hello, World!
	} else {
		fmt.Println("v is not string")
	}
}
类型切换

结合 switch...case 语句,处理接口变量可能包含多种类型的情况,基本语法格式如下所示:

switch v := interfaceVariable.(type) {
case Type1:
    // 处理 Type1
case Type2:
    // 处理 Type2
default:
    // 默认处理
}

类型切换适用于接口变量可能包含多种类型的情况,可以在多个类型之间进行分支处理。另外,与类型断言相比,类型切换可以优雅地处理未知类型,通过 default 分支提供默认逻辑。

package main

import "fmt"

func main() {
	var v interface{} = 10

	switch v := v.(type) {
	case int:
		fmt.Println("v is int:", v) // v is int: 10
	case string:
		fmt.Println("v is string:", v)
	default:
		fmt.Println("unknown type")
	}
}

源码解析

接口从功能上来说分为两类:空接口非空接口。它们再底层实现上大致是类似的(还是有区别),但是它们的用途和约束有所不同:

  • 空接口:没有方法集合,因此可以存储任何类型的值。
  • 非空接口:有一个方法集合,只有实现了这些方法的类型才能被存储。

下面分别从这两类对接口的源码进行深度解析。

空接口

空接口底层实现基于一个结构体,通常被称为接口类型描述符。定义如下所示:

源码位置:src/runtime/runtime2.go

type eface struct {
	_type *_type
	data  unsafe.Pointer
}
  • _type:指向一个类型描述符,它包含了值的类型信息,例如类型名称、大小、方法集合等。
  • data:是一个指针,指向实际存储的值。由于 data 是一个通用指针,它可以指向任何类型的值。
类型描述符

源码位置:src/runtime/type.go

type _type = abi.Type

源码位置:src/internal/abi/type.go

type Type struct {
	Size_       uintptr
	PtrBytes    uintptr // number of (prefix) bytes in the type that can contain pointers
	Hash        uint32  // hash of type; avoids computation in hash tables
	TFlag       TFlag   // extra type information flags
	Align_      uint8   // alignment of variable with this type
	FieldAlign_ uint8   // alignment of struct field with this type
	Kind_       Kind    // enumeration for C
	// function for comparing objects of this type
	// (ptr to object A, ptr to object B) -> ==?
	Equal func(unsafe.Pointer, unsafe.Pointer) bool
	// GCData stores the GC type data for the garbage collector.
	// If the KindGCProg bit is set in kind, GCData is a GC program.
	// Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
	GCData    *byte
	Str       NameOff // string form
	PtrToThis TypeOff // type for pointer to this type, may be zero
}
  • Size_:类型的大小(以字节为单位)。例如,int32 的大小为 4 字节,int64 的大小为 8 字节。
  • PtrBytes:类型中可能包含指针的前缀部分的字节数。这个字段主要用于垃圾回收器,帮助 gc 确定哪些部分可能包含指针。
  • Hash:类型的哈希值,用于在哈希表中快速比较和查找类型。
  • TFlag:类型标志,存储额外的类型信息标志。
  • Align_:类型的对齐要求。例如,int64 在内存中需要 8 字节对齐。
  • FieldAlign_:作为结构体字段时的对齐要求。
  • Kind_:类型的种类(Kind),是一个枚举值,例如 reflect.Intreflect.Stringreflect.Struct 等。
  • Equal:一个函数指针,用于比较两个该类型的值是否相等。例如,对于结构体类型,Equal 函数会比较结构体的每个字段是否相等。
  • GCData:垃圾回收相关的数据。如果 Kind 中设置了 KindGCProg 标志,则 GCData 是一个垃圾回收程序;否则是一个指针掩码(ptrmask)。
  • Str:类型的字符串表示(NameOff 是一个偏移量,指向类型名称的字符串)。
  • PtrToThis:指向该类型的指针类型的偏移量。例如,如果 Type 表示 int,则 PtrToThis 表示 *int
输出接口值

给出一段打印空接口值的代码,如下所示:

package main

func main() {
	var v interface{} = 10
	print(v) // (0x46d400,0x4967e8)
}

将上面的代码编译成汇编代码,如下所示:

0x000e 00014        CALL    runtime.printlock(SB)
# ...
0x0021 00033        CALL    runtime.printeface(SB)
0x0026 00038        CALL    runtime.printunlock(SB)

以上汇编代码只是部分截取,请注意甄别。

printeface 函数是运行时打印接口值所执行的函数,如下所示:

源码位置:src/runtime/print.go

func printeface(e eface) {
	print("(", e._type, ",", e.data, ")")
}

经过 print 函数执行后,将类型描述符的地址和底层实际存储的值的地址格式化打印出来。

非空接口

非空接口底层实现也是基于一个结构体,通常被称为接口类型描述符。定义如下所示:

源码位置:src/runtime/runtime2.go

type iface struct {
	tab  *itab
	data unsafe.Pointer
}
  • tab:是一个指向 ITab 的指针,ITab 是一个重要的结构体,用于存储接口类型和具体实现类型之间的映射关系。

  • data:是一个指针,指向实际存储的值。由于 data 是一个通用指针,它可以指向任何类型的值。

ITab

源码位置:src/runtime/runtime2.go

type itab = abi.ITab

ITab 是一个运行时结构体,用于表示接口类型和具体实现类型之间的映射关系。它的定义如下所示:

源码位置:src/internal/abi/iface.go

type ITab struct {
	Inter *InterfaceType
	Type  *Type
	Hash  uint32     // copy of Type.Hash. Used for type switches.
	Fun   [1]uintptr // variable sized. fun[0]==0 means Type does not implement Inter.
}
  • Inter*InterfaceType):接口类型的类型描述符。这表示接口的类型信息,例如接口中定义的方法集合。
  • Type*Type):具体实现类型的类型描述符。这表示接口背后的实际类型。
  • Hashuint32):用于类型切换的哈希值。它是 Type.Hash 的副本,用于快速比较类型。
  • Fun[1]uintptr):一个可变大小的数组,存储接口方法到具体实现方法的映射(函数指针)。Fun 数组的大小取决于接口中定义的方法数量。
itabTableType

itabTableTypegolang 运行时用来存储和管理所有 itab 的数据结构。它是一个全局的哈希表,用于缓存已经生成的 itab,以避免重复创建。

源码位置:src/runtime/iface.go

const itabInitSize = 512

var (
	itabLock      mutex                               // lock for accessing itab table
	itabTable     = &itabTableInit                    // pointer to current table
	itabTableInit = itabTableType{size: itabInitSize} // starter table
)

// Note: change the formula in the mallocgc call in itabAdd if you change these fields.
type itabTableType struct {
	size    uintptr             // length of entries array. Always a power of 2.
	count   uintptr             // current number of filled entries.
	entries [itabInitSize]*itab // really [size] large
}
  • size:哈希表的大小(即 entries 数组的长度)。它始终是 2 的幂次方。
  • count:当前哈希表中已填充的条目数量(即已存储的 itab 数量)。
  • entries:哈希表的条目指针数组,存储指向 itab 的指针。数组的初始长度是512。
初始化 itab

golang 程序启动时,运行时会执行一系列初始化操作,包括初始化全局锁、内存分配器等。在这些初始化操作中,itabsinit 函数会被调用,用于初始化全局的 itab 表。

源码位置:src/runtime/iface.go

func itabsinit() {
    // 初始化全局锁 itabLock
	lockInit(&itabLock, lockRankItab)
	lock(&itabLock)
    // 获取所有加载的模块
	for _, md := range activeModules() {
		for _, i := range md.itablinks {
            // 将每个模块的 itab 添加到全局表中
			itabAdd(i)
		}
	}
	unlock(&itabLock)
}

itabsinit 函数会遍历所有加载的模块,将每个模块的 itab 添加到全局表中。

添加 itab

itabAdd 函数的主要作用是将一个新的 itab 添加到全局的 itab 表中。

源码位置:src/runtime/iface.go

// itabAdd adds the given itab to the itab hash table.
// itabLock must be held.
func itabAdd(m *itab) {
	// Bugs can lead to calling this while mallocing is set,
	// typically because this is called while panicking.
	// Crash reliably, rather than only when we need to grow
	// the hash table.
    // 检查当前 Goroutine 是否处于内存分配中
    // 如果在内存分配中调用 itabAdd,会触发死锁,因此直接抛出错误
	if getg().m.mallocing != 0 {
		throw("malloc deadlock")
	}

	t := itabTable
    // 检查哈希表的负载因子是否超过 75%
	if t.count >= 3*(t.size/4) { // 75% load factor
        // 哈希表扩容的逻辑
		// Grow hash table.
		// t2 = new(itabTableType) + some additional entries
		// We lie and tell malloc we want pointer-free memory because
		// all the pointed-to values are not in the heap.
        // 分配新的哈希表,大小为原来的两倍
		t2 := (*itabTableType)(mallocgc((2+2*t.size)*goarch.PtrSize, nil, true))
		t2.size = t.size * 2
		
        // 在复制时,其他线程可能会寻找 itab 而找不到它。没关系,然后它们将尝试获取 tab 锁,并因此等待,直到复制完成
		// Copy over entries.
		// Note: while copying, other threads may look for an itab and
		// fail to find it. That's ok, they will then try to get the itab lock
		// and as a consequence wait until this copying is complete.
        // 遍历当前哈希表中的所有 itab,并将它们添加到新的哈希表中
        // 传入 add 函数指针,其实调用的还是 add 函数进行添加
		iterate_itabs(t2.add)
		if t2.count != t.count {
			throw("mismatched count during itab table copy")
		}
		// Publish new hash table. Use an atomic write: see comment in getitab.
        // 原子地更新全局 itabTable 指针,确保线程安全
		atomicstorep(unsafe.Pointer(&itabTable), unsafe.Pointer(t2))
		// Adopt the new table as our own.
		t = itabTable
		// Note: the old table can be GC'ed here.
	}
    
    // 如果没有超过 75%,则调用 itabTableType.add 方法,将新的 itab 添加到哈希表中
	t.add(m)
}

另外,在扩容完成后,新的 itab 表需要被发布。为了确保线程安全,golang 使用原子操作 atomicstorep 来更新全局 itabTable 指针,这样可以确保在多线程环境下,itabTable 的更新是线程安全的。

接下来就是将 itab 添加到哈希表中的代码 itabTableType.add

源码位置:src/runtime/iface.go

// add adds the given itab to itab table t.
// itabLock must be held.
func (t *itabTableType) add(m *itab) {
	// See comment in find about the probe sequence.
	// Insert new itab in the first empty spot in the probe sequence.
    // 使用掩码 mask 确保哈希值在哈希表的范围内
	mask := t.size - 1
    // 使用 itabHashFunc 计算 itab 的哈希值
	h := itabHashFunc(m.Inter, m.Type) & mask
	for i := uintptr(1); ; i++ {
        // 使用线性探测法解决哈希冲突
		p := (**itab)(add(unsafe.Pointer(&t.entries), h*goarch.PtrSize))
		m2 := *p
        // 如果出现相同的 itab 则直接返回,避免重复添加
		if m2 == m {
			// A given itab may be used in more than one module
			// and thanks to the way global symbol resolution works, the
			// pointed-to itab may already have been inserted into the
			// global 'hash'.
			return
		}
        // 如果找到空位,将新的 itab 添加到该位置
		if m2 == nil {
			// Use atomic write here so if a reader sees m, it also
			// sees the correctly initialized fields of m.
			// NoWB is ok because m is not in heap memory.
			// *p = m
            // 使用 atomic.StorepNoWB 确保写入操作是原子的
			atomic.StorepNoWB(unsafe.Pointer(p), unsafe.Pointer(m))
            // 更改全局哈希表的计数值
			t.count++
			return
		}
        
        // 更新哈希值
        // 直接查找下一个,线性探测法
		h += i
		h &= mask
	}
}
查找 itab

getitab 函数的作用是查找或生成一个 itab。如果找到现成的 itab,则直接返回;如果没有找到,则动态生成一个新的 itab 并添加到全局表中。

源码位置:src/runtime/iface.go

// getitab should be an internal detail,
// but widely used packages access it using linkname.
// Notable members of the hall of shame include:
//   - github.com/bytedance/sonic
//
// Do not remove or change the type signature.
// See go.dev/issue/67401.
//
//go:linkname getitab
func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
    // 如果接口没有方法,抛出内部错误
    // 必须保证是非空接口
	if len(inter.Methods) == 0 {
		throw("internal error - misuse of itab")
	}

	// easy case
    // 如果具体类型没有方法表, 并且 canfail 为 true,则返回 nil
	if typ.TFlag&abi.TFlagUncommon == 0 {
		if canfail {
			return nil
		}
        // 如果 canfail 为 false,抛出类型断言错误
		name := toRType(&inter.Type).nameOff(inter.Methods[0].Name)
		panic(&TypeAssertionError{nil, typ, &inter.Type, name.Name()})
	}

	var m *itab

	// First, look in the existing table to see if we can find the itab we need.
	// This is by far the most common case, so do it without locks.
	// Use atomic to ensure we see any previous writes done by the thread
	// that updates the itabTable field (with atomic.Storep in itabAdd).
    // 使用原子操作加载全局 itabTable
	t := (*itabTableType)(atomic.Loadp(unsafe.Pointer(&itabTable)))
    // 调用 t.find 查找现有的 itab
	if m = t.find(inter, typ); m != nil {
        // 如果找到,跳转到 finish
		goto finish
	}
	
    // 如果没有找到,先加锁,确保线程安全
	// Not found.  Grab the lock and try again.
	lock(&itabLock)
    // 再次查找
	if m = itabTable.find(inter, typ); m != nil {
        // 找到了直接就解锁,跳转到 finish
		unlock(&itabLock)
		goto finish
	}
	
    // 如果代码走到了这里,表示全局 itabTable 表中不存在目标 itab
    // 下面就需要动态生成 itab,并加入到全局 itabTable 表中
	// Entry doesn't exist yet. Make a new entry & add it.
    // 动态分配新的 itab
	m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.Methods)-1)*goarch.PtrSize, 0, &memstats.other_sys))
    // 初始化 itab 的字段
	m.Inter = inter
	m.Type = typ
	// The hash is used in type switches. However, compiler statically generates itab's
	// for all interface/type pairs used in switches (which are added to itabTable
	// in itabsinit). The dynamically-generated itab's never participate in type switches,
	// and thus the hash is irrelevant.
	// Note: m.Hash is _not_ the hash used for the runtime itabTable hash table.
	m.Hash = 0
    // 调用 itabInit 初始化方法表
	itabInit(m, true)
    // 调用 itabAdd 将新的 itab 添加到全局表中
	itabAdd(m)
    // 解锁
	unlock(&itabLock)
    
    // 接下来就是查找完成的逻辑
finish:
    // 如果 m.Fun[0] 不为 0,返回 itab
    // 这里需要解释一下,为什么这里需要加这么一个判断
    // 因为 m.Fun[0] 是这个方法表的第一个元素,通常对应接口的第一个方法
    // m.Fun[0] != 0 就表示接口的第一个方法已经被正确加载,也就是说 itab 已经初始化完毕了
    // m.Fun[0] == 0 表示接口的第一个方法没有被加载,itab初始化失败
	if m.Fun[0] != 0 {
		return m
	}
    
    // 如果 canfail 为 true,返回 nil
	if canfail {
		return nil
	}
	// this can only happen if the conversion
	// was already done once using the , ok form
	// and we have a cached negative result.
	// The cached result doesn't record which
	// interface function was missing, so initialize
	// the itab again to get the missing function name.
    // 否则,抛出类型断言错误
	panic(&TypeAssertionError{concrete: typ, asserted: &inter.Type, missingMethod: itabInit(m, false)})
}

find 方法的作用就是在 itabTable 中查找给定的接口类型和具体类型的 itab

源码位置:src/runtime/iface.go

// find finds the given interface/type pair in t.
// Returns nil if the given interface/type pair isn't present.
func (t *itabTableType) find(inter *interfacetype, typ *_type) *itab {
	// Implemented using quadratic probing.
	// Probe sequence is h(i) = h0 + i*(i+1)/2 mod 2^k.
	// We're guaranteed to hit all table entries using this probe sequence.
	mask := t.size - 1
    // 计算接口类型和具体类型的哈希值
	h := itabHashFunc(inter, typ) & mask
	for i := uintptr(1); ; i++ {
        // 计算当前哈希位置的指针
		p := (**itab)(add(unsafe.Pointer(&t.entries), h*goarch.PtrSize))
		// Use atomic read here so if we see m != nil, we also see
		// the initializations of the fields of m.
		// m := *p
        // 使用原子操作加载 itab,确保线程安全
		m := (*itab)(atomic.Loadp(unsafe.Pointer(p)))
		if m == nil {
            // 如果当前位置为空,返回 nil,表示未找到匹配的 itab
			return nil
		}
        // 如果当前 itab 的接口类型和具体类型与目标匹配,返回该 itab
		if m.Inter == inter && m.Type == typ {
			return m
		}
        
        // 当前位置不为空,并且与目标类型也不匹配,则直接查找下一个
        // 线性探测法
        
        // 更新哈希值
		h += i
		h &= mask
	}
}

在上面的代码中,使用了原子操作 atomic.Loadp 来加载 itab,确保线程安全。这避免了在多线程环境下可能出现的读取未初始化或部分初始化的 itab 的问题。

另外,在 getitab 函数中,当 itab 不存在时,会创建一个新的 itab ,并进行初始化,那么其中还有一个非常重要的初始化动作:itabInit 函数,这个函数是 golang 运行时中用于初始化 itab 的方法表的关键函数。它的作用是将接口类型的方法与具体类型的方法进行匹配,并填充 itabFun 字段。

源码位置:src/runtime/iface.go

func itabInit(m *itab, firstTime bool) string {
    // 接口类型
	inter := m.Inter
    // 具体类型
	typ := m.Type
    // 具体类型的 UncommonType,包含方法表信息
	x := typ.Uncommon()

	// both inter and typ have method sorted by name,
	// and interface names are unique,
	// so can iterate over both in lock step;
	// the loop is O(ni+nt) not O(ni*nt).
    // 接口方法的数量
	ni := len(inter.Methods)
    // 具体类型方法的数量
	nt := int(x.Mcount)
    // 具体类型的方法表
	xmhdr := (*[1 << 16]abi.Method)(add(unsafe.Pointer(x), uintptr(x.Moff)))[:nt:nt]
	j := 0
    // itab 的方法表,用于存储接口方法的函数指针
	methods := (*[1 << 16]unsafe.Pointer)(unsafe.Pointer(&m.Fun[0]))[:ni:ni]
    // 第一个方法的函数指针,用于初始化 m.Fun[0]
	var fun0 unsafe.Pointer
imethods:
    // 遍历接口的所有方法
	for k := 0; k < ni; k++ {
        // 当前接口方法的描述
		i := &inter.Methods[k]
        // 当前接口方法的类型
		itype := toRType(&inter.Type).typeOff(i.Typ)
        // 当前接口方法的名称
		name := toRType(&inter.Type).nameOff(i.Name)
        // 当前接口方法的名称字符串
		iname := name.Name()
        // 当前接口方法的包路径
		ipkg := pkgPath(name)
		if ipkg == "" {
			ipkg = inter.PkgPath.Name()
		}
        // 遍历具体类型的所有方法,尝试找到匹配的接口方法
		for ; j < nt; j++ {
            // 当前具体类型方法的描述
			t := &xmhdr[j]
            // 当前具体类型的类型信息
			rtyp := toRType(typ)
            // 当前具体类型方法的名称
			tname := rtyp.nameOff(t.Name)
            
            // 下面开始真正的匹配动作
            // 匹配条件:1. 方法类型匹配
            // 2. 方法名匹配
            // 3. 包路径匹配
			if rtyp.typeOff(t.Mtyp) == itype && tname.Name() == iname {
				pkgPath := pkgPath(tname)
				if pkgPath == "" {
					pkgPath = rtyp.nameOff(x.PkgPath).Name()
				}
				if tname.IsExported() || pkgPath == ipkg {
					ifn := rtyp.textOff(t.Ifn)
                    // 如果是第一个方法,存储函数指针到 fun0
					if k == 0 {
						fun0 = ifn // we'll set m.Fun[0] at the end
					} else if firstTime {
                        // 如果是第一次初始化,存储函数指针到 methods[k]
						methods[k] = ifn
					}
					continue imethods
				}
			}
		}
		// didn't find method
		// Leaves m.Fun[0] set to 0.
        // 如果没有找到匹配的方法,返回缺失方法的名称
        // m.Fun[0] 保持为 0,其实就是表示了接口的第一个方法未被加载
		return iname
	}
    
    // 如果是第一次初始化,设置 m.Fun[0] 为第一个方法的函数指针
	if firstTime {
		m.Fun[0] = uintptr(fun0)
	}
    // 返回空字符串,表示初始化成功
	return ""
}

常见操作原理

多态性

当一个变量被声明为接口类型时,golang 运行时会动态地管理接口的调用逻辑。主要分为两阶段:接口赋值方法调用

这里的多态性主要讨论的是非空接口。

接口赋值

当一个具体类型被赋值给接口变量时,golang 运行时会执行以下操作:

  • 首先,检查该类型是否实现了接口。
  • 如果实现了接口,运行时会创建一个 itab,并将接口类型和具体类型关联起来。
  • 接口变量会存储指向 itab 的指针和具体类型的值。
方法调用

当调用接口方法时,golang 运行时会通过以下步骤找到具体的方法实现:

  • 通过接口变量的 itab 指针找到对应的 itab
  • itab 的方法表 Fun 中查找方法的函数指针。
  • 调用函数指针指向的具体方法。
代码示例

以下是一个完整的示例,展示接口多态的实现:

package main

import "fmt"

type Animal interface {
	GetName() string
}

type Person struct {
	Name string
	Age  int
}

type Cat struct {
	Name string
	Age  int
}

func (p *Person) GetName() string {
	return p.Name
}

func (c *Cat) GetName() string {
	return c.Name
}

func main() {
	var p Animal = &Person{Name: "ycz", Age: 18}
	fmt.Println(p.GetName()) // ycz

	p = &Cat{Name: "ccc", Age: 1}
	fmt.Println(p.GetName()) // ccc
}

将上述代码编译成汇编代码如下所示:

# ...

# 分配内存
0x001a 00026      LEAQ    type:main.Person(SB), AX
0x0021 00033      PCDATA  $1, $0
0x0021 00033      CALL    runtime.newobject(SB)
0x0026 00038      MOVQ    AX, main..autotmp_5+88(SP)

# 初始化 Person 对象
0x002b 00043      MOVQ    $3, 8(AX)
0x0033 00051      CMPL    runtime.writeBarrier(SB), $0
0x003a 00058      PCDATA  $0, $-2
0x003a 00058      JEQ     64
0x003c 00060      JMP     66
0x003e 00062      NOP
0x0040 00064      JMP     79
0x0042 00066      MOVQ    (AX), CX
0x0045 00069      CALL    runtime.gcWriteBarrier1(SB)
0x004a 00074      MOVQ    CX, (R11)
0x004d 00077      JMP     79
0x004f 00079      LEAQ    go:string."ycz"(SB), CX
0x0056 00086      MOVQ    CX, (AX)

# ... 

# 接口赋值
0x0075 00117      LEAQ    go:itab.*main.Person,main.Animal(SB), CX
# 更新 itab 指针
0x007c 00124      MOVQ    CX, main.p+24(SP)
# 更新 data 指针
0x0081 00129      MOVQ    AX, main.p+32(SP)
# 调用 main.(*Person).GetName 方法
0x0086 00134      CALL    main.(*Person).GetName(SB)
0x008b 00139      MOVQ    AX, main..autotmp_2+120(SP)
0x0090 00144      MOVQ    BX, main..autotmp_2+128(SP)
0x0098 00152      MOVUPS  X15, main..autotmp_3+104(SP)
0x009e 00158      LEAQ    main..autotmp_3+104(SP), CX
0x00a3 00163      MOVQ    CX, main..autotmp_7+56(SP)
0x00a8 00168      MOVQ    main..autotmp_2+120(SP), AX
0x00ad 00173      MOVQ    main..autotmp_2+128(SP), BX
0x00b5 00181      PCDATA  $1, $1
# 将方法返回的结果转换为字符串类型
0x00b5 00181      CALL    runtime.convTstring(SB)

# ...

# 输出结果
0x0120 00288      CALL    fmt.Println(SB)

# 创建另一个对象 Cat
0x0125 00293      LEAQ    type:main.Cat(SB), AX
# 分配内存并存储对象地址
0x012c 00300      CALL    runtime.newobject(SB)
0x0131 00305      MOVQ    AX, main..autotmp_9+40(SP)
0x0136 00310      MOVQ    $3, 8(AX)
0x013e 00318      CMPL    runtime.writeBarrier(SB), $0
0x0145 00325      PCDATA  $0, $-2
0x0145 00325      JEQ     329
0x0147 00327      JMP     331
0x0149 00329      JMP     344
0x014b 00331      MOVQ    (AX), CX
0x014e 00334      CALL    runtime.gcWriteBarrier1(SB)
0x0153 00339      MOVQ    CX, (R11)
0x0156 00342      JMP     344
0x0158 00344      LEAQ    go:string."ccc"(SB), CX
0x015f 00351      MOVQ    CX, (AX)
0x0162 00354      PCDATA  $0, $-1
0x0162 00354      MOVQ    main..autotmp_9+40(SP), CX
0x0167 00359      TESTB   AL, (CX)
0x0169 00361      MOVQ    $1, 16(CX)
0x0171 00369      MOVQ    main..autotmp_9+40(SP), AX
0x0176 00374      MOVQ    AX, main..autotmp_4+96(SP)
# 将 Cat 对象赋值给接口变量
0x017b 00379      LEAQ    go:itab.*main.Cat,main.Animal(SB), CX
# 更新 itab 指针
0x0182 00386      MOVQ    CX, main.p+24(SP)
# 更新 data 指针
0x0187 00391      MOVQ    AX, main.p+32(SP)
# 调用 main.(*Cat).GetName 方法
0x018c 00396      CALL    main.(*Cat).GetName(SB)

# ...

# 将方法返回的结果转换为字符串类型
0x01c0 00448      CALL    runtime.convTstring(SB)

# ...

# 输出结果
0x023a 00570      CALL    fmt.Println(SB)

# 函数结束,返回
0x023f 00575      ADDQ    $184, SP
0x0246 00582      POPQ    BP
0x0247 00583      RET

# ...

以上汇编代码只是部分截取,请注意甄别。

从以上汇编代码就可以看出,当接口变量的类型切换时会更新 itab 指针和 data 指针,后续调用方法时,直接根据 itab 指针找到对应方法表,再从方法表中根据函数的次序定位到具体的函数(得到函数指针),再根据函数指针执行具体的方法。这种机制使得接口变量可以在运行时动态地切换类型,同时保持高效的性能和灵活的多态性。

类型断言

下面给出一个使用接口进行类型断言的例子,如下所示:

package main

func someFunction() interface{} {
	return 10
}

func main() {
	var v interface{} = someFunction()
	if i, ok := v.(int); ok {
		print(i) // 10
	}
}

将上面的代码编译成汇编代码,如下所示:

# ...
# 调用 someFunction 函数并赋值给 v
# AX 寄存器存储接口的类型信息
# BX 寄存器存储接口的数据指针
0x0012 00018       CALL    main.someFunction(SB)
0x0017 00023       MOVQ    AX, main.v+40(SP)
0x001c 00028       MOVQ    BX, main.v+48(SP)

# 类型断言的运行时检查
# 将 int 类型的地址加载到寄存器 CX
0x0021 00033       LEAQ    type:int(SB), CX
# 比较 v 的类型信息与目标类型 int 的类型信息
0x0028 00040       CMPQ    AX, CX
# 如果类型匹配跳转到成功分支地址 47
0x002b 00043       JEQ     47
# 如果类型不匹配跳转到失败分支地址 57
0x002d 00045       JMP     57

# 类型断言成功分支
# 将 v 的数据指针(存储10)指向的实际值加载到寄存器 AX
0x002f 00047       MOVQ    (BX), AX
# 将 1 赋值给寄存器 CX,表示 ok 为 true
0x0032 00050       MOVL    $1, CX
# 跳转到后续的赋值逻辑
0x0037 00055       JMP     63

# 类型断言失败分支
# 将寄存器 AX 和 CX 清零
# AX 表示 i 的值,清零表示 i 为 0
# CX 表示 ok 的值,清零表示 ok 为 false
0x0039 00057       XORL    AX, AX
0x003b 00059       XORL    CX, CX
# 跳转到后续的赋值逻辑
0x003d 00061       JMP     63

# 将寄存器中的值赋值给栈上的临时变量,最终赋值给 i 和 ok
# ...
# i 的值
0x0060 00096       MOVQ    AX, main.i+16(SP)
# ...
# ok 的值
0x006a 00106       MOVB    AL, main.ok+13(SP)

# 测试 ok 的值
0x006e 00110       TESTB   AL, AL
# 如果 ok 为 true,跳转到打印逻辑
0x0070 00112       JNE     116
# 如果 ok 为 false,跳过打印逻辑,直接结束函数
0x0072 00114       JMP     140

# 打印逻辑
# 如果类型断言成功,则调用 runtime.printint 打印 i 的值
0x0074 00116       CALL    runtime.printlock(SB)
0x0079 00121       MOVQ    main.i+16(SP), AX
0x007e 00126       NOP
0x0080 00128       CALL    runtime.printint(SB)
0x0085 00133       CALL    runtime.printunlock(SB)

# 结束函数
# ...
0x008c 00140       JMP     142
0x008e 00142       ADDQ    $56, SP
0x0092 00146       POPQ    BP
0x0093 00147       RET
# ...

以上汇编代码只是部分截取,请注意甄别。

仔细观察生成的汇编代码之后,不难发现问题所在:汇编代码中并没有调用 getitab 函数从全局 itab 表中进行获取 itab 进行接口判断。但是,实际上 golang 编译器在处理类型断言时,对于一些简单的情况,编译器可能会直接内联相关逻辑,生成断言类型目标的代码,从而直接判断是否与断言目标类型是否匹配,而不是调用 getitab 函数。这样会减少运行时额外的开销

如果将以上简单类型断言代码改为采用接口断言,如下所示:

package main

import "fmt"

type Animal interface {
	GetName() string
	GetAge() int
	SetName(name string) error
	SetAge(age int) error
}

type Person struct {
	Name string
	Age  int
}

func (p *Person) GetName() string {
	return p.Name
}

func (p *Person) GetAge() int {
	return p.Age
}

func (p *Person) SetName(name string) error {
	p.Name = name
	return nil
}

func (p *Person) SetAge(age int) error {
	p.Age = age
	return nil
}

func main() {
	var p Animal = &Person{Name: "ycz", Age: 18}
	if v, ok := p.(Animal); ok {
		fmt.Printf("%v\n", v)
	}
}

将以上代码编译成汇编代码,如下所示:

# ...

# 创建 Person 对象
0x001a 00026      LEAQ    type:main.Person(SB), AX
# ...
0x0021 00033      CALL    runtime.newobject(SB)
0x0026 00038      MOVQ    AX, main..autotmp_9+80(SP)
0x002b 00043      MOVQ    $3, 8(AX)

# ...

# 初始化 Person 对象的字段
0x004f 00079      LEAQ    go:string."ycz"(SB), DX
0x0056 00086      MOVQ    DX, (AX)
0x0059 00089      PCDATA  $0, $-1
0x0059 00089      MOVQ    main..autotmp_9+80(SP), DX
0x005e 00094      TESTB   AL, (DX)
0x0060 00096      MOVQ    $18, 16(DX)

# 将 Person 对象赋值给接口变量 p
0x0068 00104      MOVQ    main..autotmp_9+80(SP), DX
0x006d 00109      MOVQ    DX, main..autotmp_5+120(SP)
# 加载 Person 类型和 Animal 接口的 itab 地址到寄存器 R8
0x0072 00114      LEAQ    go:itab.*main.Person,main.Animal(SB), R8
0x0079 00121      MOVQ    R8, main.p+64(SP)
0x007e 00126      MOVQ    DX, main.p+72(SP)


# 类型断言逻辑
0x0083 00131      MOVUPS  X15, main..autotmp_6+104(SP)
0x0089 00137      MOVQ    main.p+64(SP), DX
0x008e 00142      MOVQ    main.p+72(SP), R8
# 检查类型信息是否为零
0x0093 00147      TESTQ   DX, DX
# 如果类型信息不为零,跳转到地址 154
0x0096 00150      JNE     154
# 如果类型信息为零,跳转到地址 192
0x0098 00152      JMP     192

# 调用 runtime.typeAssert 进行类型断言
0x009a 00154      MOVQ    R8, main..autotmp_14+144(SP)
0x00a2 00162      MOVQ    8(DX), BX
0x00a6 00166      LEAQ    main..typeAssert.0(SB), AX
0x00ad 00173      PCDATA  $1, $1
0x00ad 00173      CALL    runtime.typeAssert(SB)
0x00b2 00178      MOVQ    main..autotmp_14+144(SP), R8
0x00ba 00186      MOVQ    AX, DX
0x00bd 00189      JMP     194
0x00bf 00191      NOP

# 类型断言结果处理
# ... 

# 函数结束,返回
0x0209 00521      ADDQ    $208, SP
0x0210 00528      POPQ    BP
0x0211 00529      RET
# ...

以上汇编代码只是部分截取,请注意甄别。

汇编代码中最为关键的一步就是运行时调用了 typeAssert 进行类型断言,那么下面继续跟踪源码。

源码位置:src/runtime/iface.go

// typeAssert builds an itab for the concrete type t and the
// interface type s.Inter. If the conversion is not possible it
// panics if s.CanFail is false and returns nil if s.CanFail is true.
func typeAssert(s *abi.TypeAssert, t *_type) *itab {
	var tab *itab
    // 如果具体类型 t 为 nil,并且 s.CanFail 为 false,则抛出类型断言错误
	if t == nil {
		if !s.CanFail {
			panic(&TypeAssertionError{nil, nil, &s.Inter.Type, ""})
		}
	} else {
        // 具体类型 t 不为 nil,则调用 getitab 函数,尝试获取或生成一个 itab
		tab = getitab(s.Inter, t, s.CanFail)
	}

    // 如果当前架构不支持接口切换缓存,则直接返回 itab
	if !abi.UseInterfaceSwitchCache(GOARCH) {
		return tab
	}

	// Maybe update the cache, so the next time the generated code
	// doesn't need to call into the runtime.
    // 使用 cheaprand 生成一个随机数,只有当随机数满足条件时,才尝试更新缓存
    // 这是为了减少更新缓存的频率,避免频繁更新带来的性能开销
	if cheaprand()&1023 != 0 {
		// Only bother updating the cache ~1 in 1000 times.
		return tab
	}
    
    // 下面是更新缓存的逻辑
    
	// Load the current cache.
	oldC := (*abi.TypeAssertCache)(atomic.Loadp(unsafe.Pointer(&s.Cache)))

	if cheaprand()&uint32(oldC.Mask) != 0 {
		// As cache gets larger, choose to update it less often
		// so we can amortize the cost of building a new cache.
		return tab
	}
    
    // 当代码运行到这一步的时候,就需要构建一个新的接口切换缓存实例

	// Make a new cache.
    // 调用 buildTypeAssertCache 函数,根据当前缓存 oldC、具体类型 t 和 itab 构建一个新的缓存
	newC := buildTypeAssertCache(oldC, t, tab)

	// Update cache. Use compare-and-swap so if multiple threads
	// are fighting to update the cache, at least one of their
	// updates will stick.
    // 使用原子操作更新缓存
	atomic_casPointer((*unsafe.Pointer)(unsafe.Pointer(&s.Cache)), unsafe.Pointer(oldC), unsafe.Pointer(newC))

	return tab
}

TypeAssertCache 用于优化类型断言的性能。缓存具体类型和对应的 itab,以便快速判断一个接口变量是否可以断言为某个具体类型。下面介绍三个核心的缓存结构体:TypeAssertTypeAssertCacheTypeAssertCacheEntry

TypeAssert

源码位置:src/internal/abi/switch.go

type TypeAssert struct {
	Cache   *TypeAssertCache
	Inter   *InterfaceType
	CanFail bool
}
  • Cache:指向 TypeAssertCache,这是一个缓存结构,用于存储类型断言的结果。缓存的目的是减少运行时的重复计算,提高类型断言的性能。
  • Inter:指向 InterfaceType,表示目标接口类型。这个字段用于记录类型断言的目标接口类型,以便在需要时进行类型检查。
  • CanFail:布尔值,表示类型断言是否允许失败。如果 CanFailtrue,类型断言失败时会返回 nil;如果 CanFailfalse,类型断言失败时会抛出错误。
TypeAssertCache

源码位置:src/internal/abi/switch.go

type TypeAssertCache struct {
	Mask    uintptr
	Entries [1]TypeAssertCacheEntry
}
  • Mask:用于计算缓存的大小和索引。缓存的大小始终是 2 的幂,Mask 的值为 size - 1,用于快速计算索引。
  • Entries:缓存条目数组,初始大小为 1。每个条目是一个 TypeAssertCacheEntry,存储了具体类型和对应的 itab
TypeAssertCacheEntry

源码位置:src/internal/abi/switch.go

type TypeAssertCacheEntry struct {
	// type of source value (a *runtime._type)
	Typ uintptr
	// itab to use for result (a *runtime.itab)
	// nil if CanFail is set and conversion would fail.
	Itab uintptr
}
  • Typ:具体类型的指针。用于记录具体类型的信息,以便在类型断言时进行比较。
  • Itabitab 的指针。如果 CanFailtrue 且类型转换失败,则为 nil。用于记录具体类型和接口类型的映射关系,以便在类型断言时快速查找。
buildTypeAssertCache

构建类型断言缓存,源码如下所示:

源码位置:src/runtime/iface.go

func buildTypeAssertCache(oldC *abi.TypeAssertCache, typ *_type, tab *itab) *abi.TypeAssertCache {
    // 将旧缓存的条目转换为一个切片,包含所有旧的缓存条目
	oldEntries := unsafe.Slice(&oldC.Entries[0], oldC.Mask+1)

	// Count the number of entries we need.
    // 计算旧缓存中非空条目的数量,加上一个额外的条目用于终止
	n := 1
	for _, e := range oldEntries {
		if e.Typ != 0 {
			n++
		}
	}

	// Figure out how big a table we need.
	// We need at least one more slot than the number of entries
	// so that we are guaranteed an empty slot (for termination).
    // 新缓存的大小,通常是旧缓存大小的两倍,确保新缓存的负载因子不超过 50%
	newN := n * 2                         // make it at most 50% full
    // 计算 newN-1 的二进制表示中的位数,确保新缓存大小是 2 的幂
	newN = 1 << sys.Len64(uint64(newN-1)) // round up to a power of 2

	// Allocate the new table.
    // 计算新缓存的总大小,包括 TypeAssertCache 结构体和所有条目的大小
	newSize := unsafe.Sizeof(abi.TypeAssertCache{}) + uintptr(newN-1)*unsafe.Sizeof(abi.TypeAssertCacheEntry{})
    // 分配内存
	newC := (*abi.TypeAssertCache)(mallocgc(newSize, nil, true))
    // 设置新缓存的掩码,用于计算哈希值
	newC.Mask = uintptr(newN - 1)
    // 将新缓存的条目转换为一个切片
	newEntries := unsafe.Slice(&newC.Entries[0], newN)

	// Fill the new table.
    // 将新的缓存条目添加到新缓存中
	addEntry := func(typ *_type, tab *itab) {
		h := int(typ.Hash) & (newN - 1)
		for {
			if newEntries[h].Typ == 0 {
				newEntries[h].Typ = uintptr(unsafe.Pointer(typ))
				newEntries[h].Itab = uintptr(unsafe.Pointer(tab))
				return
			}
			h = (h + 1) & (newN - 1)
		}
	}
    // 遍历旧缓存的条目,将非空条目复制到新缓存中
	for _, e := range oldEntries {
		if e.Typ != 0 {
			addEntry((*_type)(unsafe.Pointer(e.Typ)), (*itab)(unsafe.Pointer(e.Itab)))
		}
	}
    // 调用上面的 addEntry 函数,将新的缓存条目添加到新缓存中
	addEntry(typ, tab)
	
    // 返回新的缓存示例
	return newC
}
类型切换

基于类型切换小结中的代码,再给出一段接口类型切换的代码,如下所示:

package main

import "fmt"

type Animal interface {
	GetName() string
	GetAge() int
	SetName(name string) error
	SetAge(age int) error
}

type Person struct {
	Name string
	Age  int
}

func (p *Person) GetName() string {
	return p.Name
}

func (p *Person) GetAge() int {
	return p.Age
}

func (p *Person) SetName(name string) error {
	p.Name = name
	return nil
}

func (p *Person) SetAge(age int) error {
	p.Age = age
	return nil
}

func main() {
	var p Animal = &Person{Name: "ycz", Age: 18}
	switch p := p.(type) {
	case Animal:
		fmt.Printf("%v\n", p)
	default:
		fmt.Printf("default")
	}
}

将以上代码编译成汇编代码,如下所示:

# ...

# 创建 Person 对象
0x001a 00026      LEAQ    type:main.Person(SB), AX
# ...
0x0021 00033      CALL    runtime.newobject(SB)
0x0026 00038      MOVQ    AX, main..autotmp_5+136(SP)
0x002e 00046      MOVQ    $3, 8(AX)

# ...

# 初始化 Person 对象的字段
0x0051 00081      LEAQ    go:string."ycz"(SB), CX
0x0058 00088      MOVQ    CX, (AX)
0x005b 00091      PCDATA  $0, $-1
0x005b 00091      MOVQ    main..autotmp_5+136(SP), CX
0x0063 00099      TESTB   AL, (CX)
0x0065 00101      MOVQ    $18, 16(CX)
0x006d 00109      MOVQ    main..autotmp_5+136(SP), CX
0x0075 00117      MOVQ    CX, main..autotmp_3+160(SP)
# 加载 Person 类型和 Animal 接口的 itab 地址到寄存器 DX
0x007d 00125      LEAQ    go:itab.*main.Person,main.Animal(SB), DX
0x0084 00132      MOVQ    DX, main.p+96(SP)
0x0089 00137      MOVQ    CX, main.p+104(SP)

# ...

# 类型断言逻辑
# 加载 Person 类型和 Animal 接口的 itab 地址到寄存器 CX
0x009d 00157      MOVL    go:itab.*main.Person,main.Animal+16(SB), CX
0x00a3 00163      MOVL    CX, main..autotmp_9+44(SP)
0x00a7 00167      MOVQ    main..autotmp_6+120(SP), CX
0x00ac 00172      MOVQ    8(CX), BX
# 加载类型断言的错误信息地址到寄存器 AX
0x00b0 00176      LEAQ    main..interfaceSwitch.0(SB), AX
0x00b7 00183      PCDATA  $1, $1
# 调用运行时的 interfaceSwitch 函数,进行接口切换
0x00b7 00183      CALL    runtime.interfaceSwitch(SB)
0x00bc 00188      MOVQ    AX, main..autotmp_10+56(SP)
0x00c1 00193      MOVQ    BX, main..autotmp_8+112(SP)

# 类型断言结果处理
# ...

# 函数结束,返回
0x023e 00574      ADDQ    $232, SP
0x0245 00581      POPQ    BP
0x0246 00582      RET
# ...

以上汇编代码只是部分截取,请注意甄别。

汇编代码中最为关键的一步就是运行时调用了 interfaceSwitch 进行接口类型切换,那么下面继续跟踪源码。

源码位置:src/runtime/iface.go

// interfaceSwitch compares t against the list of cases in s.
// If t matches case i, interfaceSwitch returns the case index i and
// an itab for the pair <t, s.Cases[i]>.
// If there is no match, return N,nil, where N is the number
// of cases.
func interfaceSwitch(s *abi.InterfaceSwitch, t *_type) (int, *itab) {
    // 将 s.Cases 转换为一个切片,包含所有分支的接口类型
	cases := unsafe.Slice(&s.Cases[0], s.NCases)

	// Results if we don't find a match.
    // 如果没有找到匹配项,返回的分支索引
	case_ := len(cases)
	var tab *itab

	// Look through each case in order.
    // 遍历所有分支,调用 getitab 检查具体类型 t 是否匹配当前分支的接口类型 c
	for i, c := range cases {
		tab = getitab(c, t, true)
        // 如果找到匹配项,更新 case_ 和 tab,并退出循环
		if tab != nil {
			case_ = i
			break
		}
	}
	
    // 如果不支持接口切换缓存,则直接返回结果
	if !abi.UseInterfaceSwitchCache(GOARCH) {
		return case_, tab
	}

	// Maybe update the cache, so the next time the generated code
	// doesn't need to call into the runtime.
    // 使用 cheaprand() 生成一个随机数,只有当随机数满足条件时,才尝试更新缓存
    // 避免频繁更新带来的性能开销
	if cheaprand()&1023 != 0 {
		// Only bother updating the cache ~1 in 1000 times.
		// This ensures we don't waste memory on switches, or
		// switch arguments, that only happen a few times.
		return case_, tab
	}
    // 使用原子操作加载当前的接口切换缓存 s.Cache
	// Load the current cache.
	oldC := (*abi.InterfaceSwitchCache)(atomic.Loadp(unsafe.Pointer(&s.Cache)))

	if cheaprand()&uint32(oldC.Mask) != 0 {
		// As cache gets larger, choose to update it less often
		// so we can amortize the cost of building a new cache
		// (that cost is linear in oldc.Mask).
		return case_, tab
	}

   	// 调用 buildInterfaceSwitchCache 函数,根据当前缓存 oldC、具体类型 t、匹配的分支索引 case_ 和 itab 构建一个新的缓存
	// Make a new cache.
	newC := buildInterfaceSwitchCache(oldC, t, case_, tab)

	// Update cache. Use compare-and-swap so if multiple threads
	// are fighting to update the cache, at least one of their
	// updates will stick.
    // 使用原子操作更新缓存
	atomic_casPointer((*unsafe.Pointer)(unsafe.Pointer(&s.Cache)), unsafe.Pointer(oldC), unsafe.Pointer(newC))

	return case_, tab
}

interfaceSwitch 同样做了缓存优化,类似于 TypeAssert 也有三个重要的结构体:InterfaceSwitchInterfaceSwitchCacheInterfaceSwitchCacheEntry

InterfaceSwitch

源码位置:src/internal/abi/switch.go

type InterfaceSwitch struct {
	Cache  *InterfaceSwitchCache
	NCases int

	// Array of NCases elements.
	// Each case must be a non-empty interface type.
	Cases [1]*InterfaceType
}
  • Cache:指向 InterfaceSwitchCache,用于缓存接口切换的结果。
  • NCases:分支的数量。
  • Cases:接口类型数组,存储所有可能的分支。
InterfaceSwitchCache

源码位置:src/internal/abi/switch.go

type InterfaceSwitchCache struct {
	Mask    uintptr                      // mask for index. Must be a power of 2 minus 1
	Entries [1]InterfaceSwitchCacheEntry // Mask+1 entries total
}
  • Mask:掩码,用于计算缓存索引。
  • Entries:缓存条目数组,存储具体类型和对应的分支索引。
InterfaceSwitchCacheEntry

源码位置:src/internal/abi/switch.go

type InterfaceSwitchCacheEntry struct {
	// type of source value (a *Type)
	Typ uintptr
	// case # to dispatch to
	Case int
	// itab to use for resulting case variable (a *runtime.itab)
	Itab uintptr
}
  • Typ:具体类型的指针。用于记录具体类型的信息,以便在接口切换时进行类型匹配。
  • Case:匹配的分支索引。表示在接口切换中,具体类型匹配的分支编号。
  • Itabitab 的指针。用于记录具体类型和接口类型的映射关系,以便在接口切换时快速查找和使用。
buildInterfaceSwitchCache

构建类型切换缓存,源码如下所示:

源码位置:src/runtime/iface.go

// buildInterfaceSwitchCache constructs an interface switch cache
// containing all the entries from oldC plus the new entry
// (typ,case_,tab).
func buildInterfaceSwitchCache(oldC *abi.InterfaceSwitchCache, typ *_type, case_ int, tab *itab) *abi.InterfaceSwitchCache {
    // 将旧缓存的条目转换为一个切片,包含所有旧的缓存条目
	oldEntries := unsafe.Slice(&oldC.Entries[0], oldC.Mask+1)

	// Count the number of entries we need.
	n := 1
    // 遍历旧缓存条目,统计非空条目数量
    // 新缓存需要包含旧缓存的所有条目,再加上一个新条目
	for _, e := range oldEntries {
		if e.Typ != 0 {
			n++
		}
	}

	// Figure out how big a table we need.
	// We need at least one more slot than the number of entries
	// so that we are guaranteed an empty slot (for termination).
    // 新缓存的大小至少是条目数量的两倍,保证哈希表的负载率不超过 50%
    // 确保哈希表大小是 2 的整数次幂
	newN := n * 2                         // make it at most 50% full
	newN = 1 << sys.Len64(uint64(newN-1)) // round up to a power of 2

	// Allocate the new table.
    // 计算新缓存的总大小,包括 InterfaceSwitchCache 结构体和条目数组
	newSize := unsafe.Sizeof(abi.InterfaceSwitchCache{}) + uintptr(newN-1)*unsafe.Sizeof(abi.InterfaceSwitchCacheEntry{})
    // 分配内存
	newC := (*abi.InterfaceSwitchCache)(mallocgc(newSize, nil, true))
    // 初始化新缓存的掩码
	newC.Mask = uintptr(newN - 1)
    // 初始化条目数组
	newEntries := unsafe.Slice(&newC.Entries[0], newN)

	// Fill the new table.
    // 将新的缓存条目添加到新缓存中
	addEntry := func(typ *_type, case_ int, tab *itab) {
		h := int(typ.Hash) & (newN - 1)
		for {
			if newEntries[h].Typ == 0 {
				newEntries[h].Typ = uintptr(unsafe.Pointer(typ))
				newEntries[h].Case = case_
				newEntries[h].Itab = uintptr(unsafe.Pointer(tab))
				return
			}
			h = (h + 1) & (newN - 1)
		}
	}
    
    // 遍历旧缓存的所有条目,将非空条目复制到新缓存
	for _, e := range oldEntries {
		if e.Typ != 0 {
			addEntry((*_type)(unsafe.Pointer(e.Typ)), e.Case, (*itab)(unsafe.Pointer(e.Itab)))
		}
	}
    // 调用上面的 addEntry 函数,最后插入新条目
	addEntry(typ, case_, tab)
	
    // 返回构建好的新缓存
	return newC
}

🌺🌺🌺撒花!

如果本文对你有帮助,就点关注或者留个👍
如果您有任何技术问题或者需要更多其他的内容,请随时向我提问。
在这里插入图片描述


http://www.kler.cn/a/580894.html

相关文章:

  • 数字水印系统(源码+文档+讲解+演示)
  • 【CentOS】搭建Radius服务器
  • AI开发利器:miniforge3无感平替Anaconda3
  • js防抖、节流函数封装
  • Python已知后序遍历和中序遍历,求先序遍历
  • 探索AIGC中的自动化生成
  • Python Flask 在网页应用程序中处理错误和异常
  • python实现的生态模拟系统
  • 牛客周赛:84:C:JAVA
  • P9242 [蓝桥杯 2023 省 B] 接龙数列--DP【巧妙解决接龙问题】
  • AI 帮我精准定位解决 ReferenceError: process is not defined (文末附AI名称)
  • Spring WebFlux:响应式编程
  • python使用venv命令创建虚拟环境(ubuntu22)
  • OSPF:虚链路
  • 零基础掌握Linux SCP命令:5分钟实现高效文件传输,小白必看!
  • Unity Dots从入门到精通 Mono和Dots通讯
  • DOCKER模式部署GITLAB
  • 回溯-子集
  • Java集合_八股场景题
  • vue2动态增删表单+表单验证