接口定义了一种标准,形容了类的行为和性能。咱们都晓得,Go 语言中的接口是所谓的 Duck Typing,实现接口的所有办法也就隐式地实现了接口,那么,它是怎么实现的呢?
数据结构
在 Go 语言中,接口分为两类:
- eface:用于示意没有办法的空接口类型变量,即 interface{} 类型的变量。
- iface:用于示意其余领有办法的接口类型变量。
eface
eface 的数据结构如下:
type eface struct {
_type *_type
data unsafe.Pointer
}
eface 有两个属性,别离是 _type 和 data,别离指向接口变量的动静类型和动静值。
再进一步看看 type 属性的构造:
type _type struct {
size uintptr // 类型大小
ptrdata uintptr // 蕴含所有指针的内存前缀的大小
hash uint32 // 类型的 hash 值
tflag tflag // 类型的 flag 标记,次要用于反射
align uint8 // 内存对齐相干
fieldAlign uint8 // 内存对齐相干
kind uint8 // 类型的编号,蕴含 Go 语言中的所有类型,如 kindBool、kindInt 等
equal func(unsafe.Pointer, unsafe.Pointer) bool // 用于比拟此对象的回调函数
gcdata *byte // 存储垃圾收集器的 GC 类型数据
str nameOff
ptrToThis typeOff
}
注:Go 语言的各种数据类型都是在 _type 字段的根底上,减少一些额定的字段来进行治理的。
来看一个 eface 变量的例子:
type T struct {
n int
s string
}
func main() {
var t = T {
n: 17,
s: "hello, interface",
}
var ei interface{} = t
println(ei)
}
ei 变量的构造对应于下图:
iface
iface 的构造如下:
type iface struct {
tab *itab
data unsafe.Pointer
}
与 eface 构造体一样,iface 存储的也是类型和值信息,不过因为 iface 还要存储接口自身的信息以及动静类型所实现的办法的信息,因而 iface 稍显简单,它的第一个字段指向一个 itab 类型构造:
type itab struct {
inter *interfacetype // 接口的类型信息
_type *_type // 动静类型信息
hash uint32 // _type.hash 的正本,当咱们想将 interface 类型转换成具体类型时,能够应用该字段疾速判断指标类型和具体类型 _type 是否统一
_ [4]byte
fun [1]uintptr // 存储接口办法集的具体实现的地址,蕴含一组函数指针,实现了接口办法的动静分派,且每次在接口产生变更时都会更新
}
进一步开展 interfacetype 构造体。源码如下:
type nameOff int32
type typeOff int32
type imethod struct {
name nameOff
ityp typeOff
}
type interfacetype struct {
typ _type // 动静类型信息
pkgpath name // 包名信息
mhdr []imethod // 接口所定义的办法列表}
iface 的示例如下:
type T struct {
n int
s string
}
func (T) M1() {}
func (T) M2() {}
type NonEmptyInterface interface {M1()
M2()}
func main() {
var t = T{
n: 18,
s: "hello, interface",
}
var i NonEmptyInterface = t
println(i)
}
变量 i 对应如下:
值接收者和指针接收者
在应用 Go 语言的过程中,在调用办法的时候,不论办法的接收者是什么类型,该类型的值和指针都能够调用,不用严格合乎接收者的类型。
须要记住的一点是:在 Go 语言中,如果实现了接收者是值类型的办法,会隐含实现接收者是指针类型的办法,反之则不成立。之所以能够应用值类型调用指针类型的办法,是语法糖的作用。如果只有指针类型实现了接口,应用值类型调用接口办法则会报错。
接口值的比拟
咱们看到,所有的接口类型其实底层都蕴含两个字段:类型和值,也被称为动静类型和动静值。因而接口值包含动静类型和动静值,在比拟接口值的时候,咱们须要别离对接口值的类型和值进行比拟。
nil 接口变量
package main
func main() {var i interface{}
var err error
println(i)
println(err)
println("i = nil:", i == nil)
println("err = nil:", err == nil)
println("i = err:", i == err)
println("")
}
// 输入后果
(0x0,0x0)
(0x0,0x0)
i = nil: true
err = nil: true
i = err: true
咱们看到,无论是空接口类型变量还是非空接口类型变量,一旦变量值为 nil,那么它们外部示意均为 (0x0,0x0),即类型信息和数据信息均为空。因而下面的变量 i 和 err 等值判断为 true。
空接口类型变量
func main() {var eif1 interface{}
var eif2 interface{}
n, m := 17, 18
eif1 = n
eif2 = m
println("eif1:", eif1)
println("eif2:", eif2)
println("eif1 = eif2:", eif1 == eif2)
eif2 = 17
println("eif1:", eif1)
println("eif2:", eif2)
println("eif1 = eif2:", eif1 == eif2)
eif2 = int64(17)
println("eif1:", eif1)
println("eif2:", eif2)
println("eif1 = eif2:", eif1 == eif2)
}
// 输入后果
eif1: (0x10ac580,0xc00007ef48)
eif2: (0x10ac580,0xc00007ef40)
eif1 = eif2: false
eif1: (0x10ac580,0xc00007ef48)
eif2: (0x10ac580,0x10eb3d0)
eif1 = eif2: true
eif1: (0x10ac580,0xc00007ef48)
eif2: (0x10ac640,0x10eb3d8)
eif1 = eif2: false
从输入后果能够看到:对于空接口类型变量,只有在 _type 和 data 所指数据内容统一(不是数据指针的值统一)的状况下,两个空接口类型变量才相等。
Go 在创立 eface 时个别会为 data 重新分配内存空间,将动静类型变量的值复制到这块内存空间,并将 data 指针指向这块内存空间。因而咱们在少数状况下看到的 data 指针值是不同的。但 Go 对于 data 的调配是有优化的,也不是每次都调配新内存空间,就像下面的 eif2 的 0x10eb3d0 和 0x10eb3d8 两个 data 指针值,显然是间接指向了一块当时创立好的静态数据区。
非空接口类型变量
func main() {
var err1 error
var err2 error
err1 = (*T)(nil)
println("err1:", err1)
println("err1 = nil:", err1 == nil)
err1 = T(5)
err2 = T(6)
println("err1:", err1)
println("err2:", err2)
println("err1 = err2:", err1 == err2)
err2 = fmt.Errorf("%d\n", 5)
println("err1:", err1)
println("err2:", err2)
println("err1 = err2:", err1 == err2)
}
// 输入后果
err1: (0x10ed120,0x0)
err1 = nil: false
err1: (0x10ed1a0,0x10eb310)
err2: (0x10ed1a0,0x10eb318)
err1 = err2: false
err1: (0x10ed1a0,0x10eb310)
err2: (0x10ed0c0,0xc000010050)
err1 = err2: false
与空接口类型变量一样,只有在 tab 和 data 所指数据内容统一的状况下,两个非空接口类型变量之间能力画等号。
空接口类型变量与非空接口类型变量
func main() {var eif interface{} = T(5)
var err error = T(5)
println("eif:", eif)
println("err:", err)
println("eif = err:", eif == err)
err = T(6)
println("eif:", eif)
println("err:", err)
println("eif = err:", eif == err)
}
// 输入后果
eif: (0x10b3b00,0x10eb4d0)
err: (0x10ed380,0x10eb4d8)
eif = err: true
eif: (0x10b3b00,0x10eb4d0)
err: (0x10ed380,0x10eb4e0)
eif = err: false
空接口类型变量和非空接口类型变量外部示意的构造有所不同,仿佛肯定不能相等。但 Go 在进行等值比拟时,类型比拟应用的是 eface 的 _type 和 iface 的 tab._type,因而就像咱们在这个例子中看到的那样,当 eif 和 err 都被赋值为 T(5) 时,两者之间是相等的。
类型转换
惯例变量转换接口变量
先看代码示例:
import "fmt"
type T struct {
n int
s string
}
func (T) M1() {}
func (T) M2() {}
type NonEmptyInterface interface {M1()
M2()}
func main() {
var t = T{
n: 17,
s: "hello, interface",
}
var ei interface{}
ei = t
var i NonEmptyInterface
i = t
fmt.Println(ei)
fmt.Println(i)
}
应用 go tool compile -S
命令查看生成的汇编代码,能够看到这两个转换过程对应了 runtime 包的两个函数:
......
0x0050 00080 (main.go:24) CALL runtime.convT2E(SB)
......
0x0089 00137 (main.go:27) CALL runtime.convT2I(SB)
......
这两个函数的源码如下:
// $GOROOT/src/runtime/iface.go
func convT2E(t *_type, elem unsafe.Pointer) (e eface) {
if raceenabled {raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2E))
}
if msanenabled {msanread(elem, t.size)
}
x := mallocgc(t.size, t, true)
typedmemmove(t, x, elem)
e._type = t
e.data = x
return
}
func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
t := tab._type
if raceenabled {raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2I))
}
if msanenabled {msanread(elem, t.size)
}
x := mallocgc(t.size, t, true)
typedmemmove(t, x, elem)
i.tab = tab
i.data = x
return
}
convT2E 用于将任意类型转换为一个 eface,convT2I 用于将任意类型转换为一个 iface。两个函数的实现逻辑类似,次要思路就是依据传入的类型信息(convT2E 的 _type 和 convT2I 的 tab._type)调配一块内存空间,并将 elem 指向的数据复制到这块内存空间中,最初传入的类型信息作为返回值构造中的类型信息,返回值构造中的数据指针指向新调配的那块内存空间。
那么 convT2E 和 convT2I 函数的类型信息从何而来?这些都依赖 Go 编译器的工作。Go 也在一直转换操作进行优化,包含对常见类型(如整型、字符串、切片等)提供一系列疾速转换函数:
// $GOROOT/src/cmd/compile/internal/gc/builtin/runtime.go
func convT16(val any) unsafe.Pointer // val 必须是一个 uint-16 相干类型的参数
func convT32(val any) unsafe.Pointer // val 必须是一个 unit-32 相干类型的参数
func convT64(val any) unsafe.Pointer // val 必须是一个 unit-64 相干类型的参数
func convTstring(val any) unsafe.Pointer // val 必须是一个字符串类型的参数
func convTslice(val any) unsafe.Pointer // val 必须是一个切片类型的参数
编译器晓得每个要转换为接口类型变量的动静类型变量的类型,会依据这一类型抉择适当的 convT2X 函数。
接口变量相互转换
接口之间相互转换的前提是类型兼容,也就是都实现了接口定义的办法。上面咱们来看一下运行时转换接口类型的办法:
func convI2I(inter *interfacetype, i iface) (r iface) {
tab := i.tab
if tab == nil {return}
if tab.inter == inter {
r.tab = tab
r.data = i.data
return
}
r.tab = getitab(inter, tab._type, false)
r.data = i.data
return
}
代码比较简单,函数参数 inter 示意接口类型,i 示意绑定了动静类型的接口变量,返回值 r 就是须要转换的新的 iface。通过后面的剖析,咱们晓得 iface 是由 tab 和 data 两个字段组成。所以,convI2I 函数真正要做的事就是找到并设置好新 iface 的 tab 和 data,就功败垂成了。
咱们还晓得,tab 是由接口类型 interfacetype 和 实体类型 _type 组成的。所以最要害的语句是 r.tab = getitab(inter, tab._type, false)
,来看一下 getitab 的外围代码:
func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
var m *itab
t := (*itabTableType)(atomic.Loadp(unsafe.Pointer(&itabTable)))
if m = t.find(inter, typ); m != nil {goto finish}
lock(&itabLock)
if m = itabTable.find(inter, typ); m != nil {unlock(&itabLock)
goto finish
}
m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys))
m.inter = inter
m._type = typ
m.hash = 0
m.init()
itabAdd(m)
unlock(&itabLock)
finish:
if m.fun[0] != 0 {return m}
if canfail {return nil}
panic(&TypeAssertionError{concrete: typ, asserted: &inter.typ, missingMethod: m.init()})
}
- 调用 atomic.Loadp 办法加载并查找现有的 itab hash table,看看是否是否能够找到所需的 itab 元素。
-
若没有找到,则调用 lock 办法对 itabLock 上锁,并再查找一次。
- 若找到,则跳到 finish 标识的收尾步骤。
- 若没有找到,则新生成一个 itab 元素,并调用 itabAdd 办法新增到全局的 hash table 中。
- 返回所需的 itab。