网站建设需求设计,地推拉新app推广平台,泉州网官网,拼多多网店#x1f4a2;欢迎来到张胤尘的开源技术站 #x1f4a5;开源如江河#xff0c;汇聚众志成。代码似星辰#xff0c;照亮行征程。开源精神长#xff0c;传承永不忘。携手共前行#xff0c;未来更辉煌#x1f4a5; 文章目录 接口接口定义接口初始化接口嵌套空接口存储任意类… 欢迎来到张胤尘的开源技术站 开源如江河汇聚众志成。代码似星辰照亮行征程。开源精神长传承永不忘。携手共前行未来更辉煌 文章目录 接口接口定义接口初始化接口嵌套空接口存储任意类型的数据作为函数参数类型断言和类型切换类型断言类型切换 源码解析空接口类型描述符输出接口值 非空接口ITabitabTableType初始化 itab 表添加 itab查找 itab 常见操作原理多态性接口赋值方法调用代码示例 类型断言TypeAssertTypeAssertCacheTypeAssertCacheEntrybuildTypeAssertCache 类型切换InterfaceSwitchInterfaceSwitchCacheInterfaceSwitchCacheEntrybuildInterfaceSwitchCache 接口
接口是一种抽象的数据类型它定义了一组方法或行为的规范但不提供具体实现。在 golang 中接口的核心作用是解耦合 和 多态。 解耦合减少模块之间的直接依赖关系使得模块之间的修改不会相互影响。另外通过接口调用者只需要知道接口的规范而不需要关心具体的实现细节。实现者可以随时替换只要满足接口规范即可。 多态多态是指同一个接口可以被不同的实现类使用调用者在运行时可以根据具体实现调用相应的方法。接口允许不同的实现类提供不同的行为但调用者可以通过统一的接口进行操作。
接口定义
在 golang 中接口通过 type 关键字和 interface 关键字定义。语法如下所示
type InterfaceName interface {Method1(paramType1) returnType1Method2(paramType2) returnType2...
}例如
type Shape interface {Area() float64Perimeter() 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 maintype Shape interface {Area() float64Perimeter() float64
}func main() {var s Shape nils.Area() // invalid memory address or nil pointer dereference
}则需要为其指定正确的实现类来完成初始化行为如下所示
package mainimport (fmtmath
)type Shape interface {Area() float64Perimeter() 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 Shapeshape 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通过嵌套 Reader 和 Writer它继承了这两个接口的所有方法。如下所示
type ReadWriter interface {ReaderWriter
}由于 ReadWriter 继承了 Reader 和 Writer 两个接口所以实现 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 ReadWriterrw 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 是一个空接口变量它可以存储任何类型的值。
在实际开发过程中会经常使用到空接口例如存储任意类型的数据、作为函数参数、类型断言和类型切换。
存储任意类型的数据
空接口可以存储任何类型的值包括基本类型如 int、string、自定义类型如结构体、切片、映射等。如下所示
package mainimport fmtfunc main() {var v interface{}v 42fmt.Println(v) // 42v zycfmt.Println(v) // zycv []int{1, 2, 3}fmt.Println(v) // [1 2 3]
}作为函数参数
空接口常用于函数参数使函数能够接受任何类型的值。如下所示
package mainimport fmtfunc PrintValue(v interface{}) {fmt.Println(v)
}func main() {PrintValue(10) // 10PrintValue(zzzzzzz) // zzzzzzzPrintValue([]int{1, 2, 3}) // [1 2 3]
}类型断言和类型切换
由于空接口存储的值类型不明确通常需要通过 类型断言 或 类型切换 来获取其实际类型。
类型断言
用于从接口变量中提取具体的类型值。基本语法格式如下所示
value, ok : interfaceVariable.(Type)value : iface.(Type)在使用过程中如果断言失败ok 会返回 false而 value 会是目标类型的零值。但是需要注意的是如果直接使用类型断言而不检查 ok 值可能导致运行时 panic。例如如果接口变量的实际类型与断言类型不匹配程序会崩溃。
package mainimport fmtfunc 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 mainimport fmtfunc main() {var v interface{} 10switch v : v.(type) {case int:fmt.Println(v is int:, v) // v is int: 10case string:fmt.Println(v is string:, v)default:fmt.Println(unknown type)}
}源码解析
接口从功能上来说分为两类空接口 和 非空接口。它们再底层实现上大致是类似的还是有区别但是它们的用途和约束有所不同
空接口没有方法集合因此可以存储任何类型的值。非空接口有一个方法集合只有实现了这些方法的类型才能被存储。
下面分别从这两类对接口的源码进行深度解析。
空接口
空接口底层实现基于一个结构体通常被称为接口类型描述符。定义如下所示 源码位置src/runtime/runtime2.go type eface struct {_type *_typedata unsafe.Pointer
}_type指向一个类型描述符它包含了值的类型信息例如类型名称、大小、方法集合等。data是一个指针指向实际存储的值。由于 data 是一个通用指针它可以指向任何类型的值。
类型描述符 源码位置src/runtime/type.go type _type abi.Type源码位置src/internal/abi/type.go type Type struct {Size_ uintptrPtrBytes uintptr // number of (prefix) bytes in the type that can contain pointersHash uint32 // hash of type; avoids computation in hash tablesTFlag TFlag // extra type information flagsAlign_ uint8 // alignment of variable with this typeFieldAlign_ uint8 // alignment of struct field with this typeKind_ 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 *byteStr NameOff // string formPtrToThis 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.Int、reflect.String、reflect.Struct 等。Equal一个函数指针用于比较两个该类型的值是否相等。例如对于结构体类型Equal 函数会比较结构体的每个字段是否相等。GCData垃圾回收相关的数据。如果 Kind 中设置了 KindGCProg 标志则 GCData 是一个垃圾回收程序否则是一个指针掩码ptrmask。Str类型的字符串表示NameOff 是一个偏移量指向类型名称的字符串。PtrToThis指向该类型的指针类型的偏移量。例如如果 Type 表示 int则 PtrToThis 表示 *int。
输出接口值
给出一段打印空接口值的代码如下所示
package mainfunc main() {var v interface{} 10print(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 *itabdata unsafe.Pointer
}tab是一个指向 ITab 的指针ITab 是一个重要的结构体用于存储接口类型和具体实现类型之间的映射关系。 data是一个指针指向实际存储的值。由于 data 是一个通用指针它可以指向任何类型的值。
ITab 源码位置src/runtime/runtime2.go type itab abi.ITabITab 是一个运行时结构体用于表示接口类型和具体实现类型之间的映射关系。它的定义如下所示 源码位置src/internal/abi/iface.go type ITab struct {Inter *InterfaceTypeType *TypeHash 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
itabTableType 是 golang 运行时用来存储和管理所有 itab 的数据结构。它是一个全局的哈希表用于缓存已经生成的 itab以避免重复创建。 源码位置src/runtime/iface.go const itabInitSize 512var (itabLock mutex // lock for accessing itab tableitabTable itabTableInit // pointer to current tableitabTableInit 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() {// 初始化全局锁 itabLocklockInit(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((22*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. Thats 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 GCed 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) maskfor 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.countreturn}// 更新哈希值// 直接查找下一个线性探测法h ih 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则返回 nilif typ.TFlagabi.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).// 使用原子操作加载全局 itabTablet : (*itabTableType)(atomic.Loadp(unsafe.Pointer(itabTable)))// 调用 t.find 查找现有的 itabif m t.find(inter, typ); m ! nil {// 如果找到跳转到 finishgoto finish}// 如果没有找到先加锁确保线程安全// Not found. Grab the lock and try again.lock(itabLock)// 再次查找if m itabTable.find(inter, typ); m ! nil {// 找到了直接就解锁跳转到 finishunlock(itabLock)goto finish}// 如果代码走到了这里表示全局 itabTable 表中不存在目标 itab// 下面就需要动态生成 itab并加入到全局 itabTable 表中// Entry doesnt exist yet. Make a new entry add it.// 动态分配新的 itabm (*itab)(persistentalloc(unsafe.Sizeof(itab{})uintptr(len(inter.Methods)-1)*goarch.PtrSize, 0, memstats.other_sys))// 初始化 itab 的字段m.Inter interm.Type typ// The hash is used in type switches. However, compiler statically generates itabs// for all interface/type pairs used in switches (which are added to itabTable// in itabsinit). The dynamically-generated itabs 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返回 nilif 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 doesnt 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 isnt present.
func (t *itabTableType) find(inter *interfacetype, typ *_type) *itab {// Implemented using quadratic probing.// Probe sequence is h(i) h0 i*(i1)/2 mod 2^k.// Were guaranteed to hit all table entries using this probe sequence.mask : t.size - 1// 计算接口类型和具体类型的哈希值h : itabHashFunc(inter, typ) maskfor 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表示未找到匹配的 itabreturn nil}// 如果当前 itab 的接口类型和具体类型与目标匹配返回该 itabif m.Inter inter m.Type typ {return m}// 当前位置不为空并且与目标类型也不匹配则直接查找下一个// 线性探测法// 更新哈希值h ih mask}
}在上面的代码中使用了原子操作 atomic.Loadp 来加载 itab确保线程安全。这避免了在多线程环境下可能出现的读取未初始化或部分初始化的 itab 的问题。
另外在 getitab 函数中当 itab 不存在时会创建一个新的 itab 并进行初始化那么其中还有一个非常重要的初始化动作itabInit 函数这个函数是 golang 运行时中用于初始化 itab 的方法表的关键函数。它的作用是将接口类型的方法与具体类型的方法进行匹配并填充 itab 的 Fun 字段。 源码位置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(nint) 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)// 如果是第一个方法存储函数指针到 fun0if k 0 {fun0 ifn // well set m.Fun[0] at the end} else if firstTime {// 如果是第一次初始化存储函数指针到 methods[k]methods[k] ifn}continue imethods}}}// didnt 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 mainimport fmttype Animal interface {GetName() string
}type Person struct {Name stringAge int
}type Cat struct {Name stringAge 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()) // yczp 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_588(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.p24(SP)
# 更新 data 指针
0x0081 00129 MOVQ AX, main.p32(SP)
# 调用 main.(*Person).GetName 方法
0x0086 00134 CALL main.(*Person).GetName(SB)
0x008b 00139 MOVQ AX, main..autotmp_2120(SP)
0x0090 00144 MOVQ BX, main..autotmp_2128(SP)
0x0098 00152 MOVUPS X15, main..autotmp_3104(SP)
0x009e 00158 LEAQ main..autotmp_3104(SP), CX
0x00a3 00163 MOVQ CX, main..autotmp_756(SP)
0x00a8 00168 MOVQ main..autotmp_2120(SP), AX
0x00ad 00173 MOVQ main..autotmp_2128(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_940(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_940(SP), CX
0x0167 00359 TESTB AL, (CX)
0x0169 00361 MOVQ $1, 16(CX)
0x0171 00369 MOVQ main..autotmp_940(SP), AX
0x0176 00374 MOVQ AX, main..autotmp_496(SP)
# 将 Cat 对象赋值给接口变量
0x017b 00379 LEAQ go:itab.*main.Cat,main.Animal(SB), CX
# 更新 itab 指针
0x0182 00386 MOVQ CX, main.p24(SP)
# 更新 data 指针
0x0187 00391 MOVQ AX, main.p32(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 mainfunc 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.v40(SP)
0x001c 00028 MOVQ BX, main.v48(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.i16(SP)
# ...
# ok 的值
0x006a 00106 MOVB AL, main.ok13(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.i16(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 mainimport fmttype Animal interface {GetName() stringGetAge() intSetName(name string) errorSetAge(age int) error
}type Person struct {Name stringAge 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 namereturn nil
}func (p *Person) SetAge(age int) error {p.Age agereturn 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_980(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_980(SP), DX
0x005e 00094 TESTB AL, (DX)
0x0060 00096 MOVQ $18, 16(DX)# 将 Person 对象赋值给接口变量 p
0x0068 00104 MOVQ main..autotmp_980(SP), DX
0x006d 00109 MOVQ DX, main..autotmp_5120(SP)
# 加载 Person 类型和 Animal 接口的 itab 地址到寄存器 R8
0x0072 00114 LEAQ go:itab.*main.Person,main.Animal(SB), R8
0x0079 00121 MOVQ R8, main.p64(SP)
0x007e 00126 MOVQ DX, main.p72(SP)# 类型断言逻辑
0x0083 00131 MOVUPS X15, main..autotmp_6104(SP)
0x0089 00137 MOVQ main.p64(SP), DX
0x008e 00142 MOVQ main.p72(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_14144(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_14144(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 函数尝试获取或生成一个 itabtab getitab(s.Inter, t, s.CanFail)}// 如果当前架构不支持接口切换缓存则直接返回 itabif !abi.UseInterfaceSwitchCache(GOARCH) {return tab}// Maybe update the cache, so the next time the generated code// doesnt 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以便快速判断一个接口变量是否可以断言为某个具体类型。下面介绍三个核心的缓存结构体TypeAssert、TypeAssertCache、TypeAssertCacheEntry。
TypeAssert 源码位置src/internal/abi/switch.go type TypeAssert struct {Cache *TypeAssertCacheInter *InterfaceTypeCanFail bool
}Cache指向 TypeAssertCache这是一个缓存结构用于存储类型断言的结果。缓存的目的是减少运行时的重复计算提高类型断言的性能。Inter指向 InterfaceType表示目标接口类型。这个字段用于记录类型断言的目标接口类型以便在需要时进行类型检查。CanFail布尔值表示类型断言是否允许失败。如果 CanFail 为 true类型断言失败时会返回 nil如果 CanFail 为 false类型断言失败时会抛出错误。
TypeAssertCache 源码位置src/internal/abi/switch.go type TypeAssertCache struct {Mask uintptrEntries [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 的指针。如果 CanFail 为 true 且类型转换失败则为 nil。用于记录具体类型和接口类型的映射关系以便在类型断言时快速查找。
buildTypeAssertCache
构建类型断言缓存源码如下所示 源码位置src/runtime/iface.go func buildTypeAssertCache(oldC *abi.TypeAssertCache, typ *_type, tab *itab) *abi.TypeAssertCache {// 将旧缓存的条目转换为一个切片包含所有旧的缓存条目oldEntries : unsafe.Slice(oldC.Entries[0], oldC.Mask1)// Count the number of entries we need.// 计算旧缓存中非空条目的数量加上一个额外的条目用于终止n : 1for _, 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 mainimport fmttype Animal interface {GetName() stringGetAge() intSetName(name string) errorSetAge(age int) error
}type Person struct {Name stringAge 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 namereturn nil
}func (p *Person) SetAge(age int) error {p.Age agereturn 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_5136(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_5136(SP), CX
0x0063 00099 TESTB AL, (CX)
0x0065 00101 MOVQ $18, 16(CX)
0x006d 00109 MOVQ main..autotmp_5136(SP), CX
0x0075 00117 MOVQ CX, main..autotmp_3160(SP)
# 加载 Person 类型和 Animal 接口的 itab 地址到寄存器 DX
0x007d 00125 LEAQ go:itab.*main.Person,main.Animal(SB), DX
0x0084 00132 MOVQ DX, main.p96(SP)
0x0089 00137 MOVQ CX, main.p104(SP)# ...# 类型断言逻辑
# 加载 Person 类型和 Animal 接口的 itab 地址到寄存器 CX
0x009d 00157 MOVL go:itab.*main.Person,main.Animal16(SB), CX
0x00a3 00163 MOVL CX, main..autotmp_944(SP)
0x00a7 00167 MOVQ main..autotmp_6120(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_1056(SP)
0x00c1 00193 MOVQ BX, main..autotmp_8112(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 dont find a match.// 如果没有找到匹配项返回的分支索引case_ : len(cases)var tab *itab// Look through each case in order.// 遍历所有分支调用 getitab 检查具体类型 t 是否匹配当前分支的接口类型 cfor i, c : range cases {tab getitab(c, t, true)// 如果找到匹配项更新 case_ 和 tab并退出循环if tab ! nil {case_ ibreak}}// 如果不支持接口切换缓存则直接返回结果if !abi.UseInterfaceSwitchCache(GOARCH) {return case_, tab}// Maybe update the cache, so the next time the generated code// doesnt need to call into the runtime.// 使用 cheaprand() 生成一个随机数只有当随机数满足条件时才尝试更新缓存// 避免频繁更新带来的性能开销if cheaprand()1023 ! 0 {// Only bother updating the cache ~1 in 1000 times.// This ensures we dont 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 也有三个重要的结构体InterfaceSwitch、InterfaceSwitchCache、InterfaceSwitchCacheEntry。
InterfaceSwitch 源码位置src/internal/abi/switch.go type InterfaceSwitch struct {Cache *InterfaceSwitchCacheNCases 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 1Entries [1]InterfaceSwitchCacheEntry // Mask1 entries total
}Mask掩码用于计算缓存索引。Entries缓存条目数组存储具体类型和对应的分支索引。
InterfaceSwitchCacheEntry 源码位置src/internal/abi/switch.go type InterfaceSwitchCacheEntry struct {// type of source value (a *Type)Typ uintptr// case # to dispatch toCase 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.Mask1)// 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% fullnewN 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
}撒花
如果本文对你有帮助就点关注或者留个 如果您有任何技术问题或者需要更多其他的内容请随时向我提问。