原文链接:
Go 当中 interface 设计(中)mp.weixin.qq.comeface
vs iface
eface
和 iface
都是 Go 中描述接口的底层数据结构, 区别在于 iface
描述的接口包含方法, 而 eface
则是不包含 任何方法的空接口: interface{}
.
接口的底层数据结构:
// 空接口, interface{} 的底层结构
type eface struct {
_type *_type
data unsafe.Pointer
}
// 方法接口, 自定义的接口的数据结构
type iface struct {
tab *itab
data unsafe.Pointer
}
type itab struct {
inter *interfacetype // 接口类型
_type *_type // 值类型
hash uint32 // 值类型 _type 当中的 hash 的拷贝
_ [4]byte
fun [1]uintptr
}
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod
}
type name struct {
bytes *byte
}
type imethod struct {
name int32
ityp int32
}
type _type struct {
size uintptr
ptrdata uintptr // 包含所有指针的内存前缀的大小. 如果为0, 表示的是一个值, 而非指针
hash uint32
tflag uint8
align uint8
fieldalign uint8
kind uint8
alg uintptr
gcdata *byte
str int32
ptrToThis int32
}
iface
维护两个指针, tab
指向一个 itab
实体, 它表示接口的类型 以及赋值给这个接口的实体类型. data
则指向接口 具体的值, 一般情况下是一个指向堆内存的指针.
itab
结构: inter
字段描述了接口的类型(静态), _type
字段描述了接口值的类型(动态), 包括内存对齐方式, 大小等. fun
字段放置接口类型的方法地址 (与接口方法对于的具体数据类型的方法地址??), 实现接口调用方法的动态分派.
为何 fun
数组的大小为1, 如果接口定义了多个方法怎么办? 实际上, 这里存储的是第一个方法的函数指针, 如果有更多的方法, 在 它之后的内存空间里继续存在. 从汇编角度来看, 通过增加地址就能获取到这些函数指针, 没什么影响. 方法是按照函数名称的字典顺序 进行排列的.
iterfacetype
类型, 它描述了接口的类型(静态). 它包装了 _type
类型, _type
是描述 Go 语言各种数据类型的结构体. mhdr
字段, 表示接口定义的函数列表, 通过这里的内容, 在反射的时候可以获取实际函数的指针. pkgpath
描述了接口的包名.
相比 iface
, eface
就比较简单, 只维护了一个 _type
字段, 表示空接口所承载的具体的实体类型. data
描述了其值.
接口的动态类型和动态值
接口类型和 nil
做比较:
接口值的零值指 动态类型
和 动态值
都为 nil. 当且仅当这两部分的值都为 nil
的状况下, 这个接口值才被认为是nil, 即 接口值 == nil
.
例子1:
type Coder interface {
code()
}
type Gopher struct {
name string
}
func (g Gopher) code() {
fmt.Printf("%s is codingn", g.name)
}
func main() {
var c Coder
fmt.Println(c == nil) // true
fmt.Printf("c: %T, %vn", c, c)
var g *Gopher
fmt.Println(g == nil) // true
c = g
fmt.Println(c == nil) // fasle, 值类型为 *Gopher, 值是 nil
fmt.Printf("c: %T, %vn", c, c)
}
例子2:
type MyError struct {}
func (i MyError) Error() string {
return "MyError"
}
func Process() error {
var err *MyError = nil
return err // 值类型为 *MyError, 值是 nil
}
func main() {
err := Process()
fmt.Println(err) // nil
fmt.Println(err == nil) // false
}
例子3:
type iface struct {
itab, data uintptr
}
func main() {
var a interface{} = nil
/*
* 等价形式:
* var t *int
* var b interface{} = t
*/
var b interface{} = (*int)(nil)
x := 5
var c interface{} = (*int)(&x)
ia := *(*iface)(unsafe.Pointer(&a)) // 值类型为 nil, 值为 nil
ib := *(*iface)(unsafe.Pointer(&b)) // 值类型为 *int, 值为 nil
ic := *(*iface)(unsafe.Pointer(&c)) // 值类型为 *int, 值为 5
fmt.Println(ia, ib, ic)
fmt.Println(*(*int)(unsafe.Pointer(ic.data)))
}
编译器自动检测类型是否实现接口
奇怪的代码:
var _ io.Writer = (*myWriter)(nil)
为啥有上面的代码, 上面的代码究竟是要干什么? 编译器会通过上述的代码检查 *myWriter
类型是否实现了 io.Writer
接口.
例子:
type myWriter struct{
}
/*
func (w myWriter) Write(p []byte) (n int, err error) {
return
}
*/
func main() {
// 检查 *myWriter 是否实现了 io.Writer 接口
var _ io.Writer = (*myWriter)(nil)
// 检查 myWriter 是否实现了 io.Writer 接口
var _ io.Writer = myWriter{}
}
注释掉为 myWriter 定义的 Writer 函数后, 运行程序, 报错信息:
*myWriter/myWriter 未实现 io.Writer 接口
, 也 就是未实现 Write 方法. 解除注释后, 运行程序不报错.
实际上, 上述赋值语句会发生隐式转换, 在转换的过程中, 编译器会检测等号右边的类型是否实现了等号左边接口所规定的函数.
接口的构造过程
先从一个例子开始:
type Person interface {
grow()
}
type Student struct {
age int
name string
}
func (p Student) grow() {
p.age += 1
return
}
func main() {
var qcrao = Person(Student{age: 18, name:"san"})
fmt.Println(qcrao)
}
使用 go tool compile -S example.go
打印汇编代码. go 版本是 1.13
"".main STEXT size=331 args=0x0 locals=0xa8
0x0000 00000 (example.go:19) TEXT "".main(SB), ABIInternal, $168-0
0x0000 00000 (example.go:19) MOVQ (TLS), CX
0x0009 00009 (example.go:19) LEAQ -40(SP), AX
0x000e 00014 (example.go:19) CMPQ AX, 16(CX)
0x0012 00018 (example.go:19) JLS 321
0x0018 00024 (example.go:19) SUBQ $168, SP
0x001f 00031 (example.go:19) MOVQ BP, 160(SP)
0x0027 00039 (example.go:19) LEAQ 160(SP), BP
0x002f 00047 (example.go:20) XORPS X0, X0
0x0032 00050 (example.go:20) MOVUPS X0, ""..autotmp_1+136(SP)
0x003a 00058 (example.go:20) MOVQ $0, ""..autotmp_1+152(SP)
0x0046 00070 (example.go:20) MOVQ $18, ""..autotmp_1+136(SP)
0x0052 00082 (example.go:20) LEAQ go.string."san"(SB), AX
0x0059 00089 (example.go:20) MOVQ AX, ""..autotmp_1+144(SP)
0x0061 00097 (example.go:20) MOVQ $3, ""..autotmp_1+152(SP)
0x006d 00109 (example.go:20) LEAQ go.itab."".Student,"".Person(SB), AX
0x0074 00116 (example.go:20) MOVQ AX, (SP)
0x0078 00120 (example.go:20) LEAQ ""..autotmp_1+136(SP), AX
0x0080 00128 (example.go:20) MOVQ AX, 8(SP)
0x0085 00133 (example.go:20) CALL runtime.convT2I(SB)
0x008a 00138 (example.go:20) MOVQ 24(SP), AX
0x008f 00143 (example.go:20) MOVQ 16(SP), CX
0x0094 00148 (example.go:20) MOVQ CX, "".qcrao+64(SP)
0x0099 00153 (example.go:20) MOVQ AX, "".qcrao+72(SP)
0x009e 00158 (example.go:22) MOVQ "".qcrao+72(SP), AX
0x00a3 00163 (example.go:22) MOVQ "".qcrao+64(SP), CX
0x00a8 00168 (example.go:22) MOVQ CX, ""..autotmp_3+80(SP)
0x00ad 00173 (example.go:22) MOVQ AX, ""..autotmp_3+88(SP)
0x00b2 00178 (example.go:22) MOVQ CX, ""..autotmp_4+56(SP)
0x00b7 00183 (example.go:22) CMPQ ""..autotmp_4+56(SP), $0
0x00bd 00189 (example.go:22) JNE 193
0x00bf 00191 (example.go:22) JMP 319
0x00c1 00193 (example.go:22) TESTB AL, (CX)
0x00c3 00195 (example.go:22) MOVQ 8(CX), AX
0x00c7 00199 (example.go:22) MOVQ AX, ""..autotmp_4+56(SP)
0x00cc 00204 (example.go:22) JMP 206
0x00ce 00206 (example.go:22) PCDATA $1, $5
0x00ce 00206 (example.go:22) XORPS X0, X0
0x00d1 00209 (example.go:22) MOVUPS X0, ""..autotmp_2+96(SP)
0x00d6 00214 (example.go:22) LEAQ ""..autotmp_2+96(SP), AX
0x00db 00219 (example.go:22) MOVQ AX, ""..autotmp_6+48(SP)
0x00e0 00224 (example.go:22) TESTB AL, (AX)
0x00e2 00226 (example.go:22) MOVQ ""..autotmp_4+56(SP), CX
0x00e7 00231 (example.go:22) MOVQ ""..autotmp_3+88(SP), DX
0x00ec 00236 (example.go:22) MOVQ CX, ""..autotmp_2+96(SP)
0x00f1 00241 (example.go:22) MOVQ DX, ""..autotmp_2+104(SP)
0x00f6 00246 (example.go:22) TESTB AL, (AX)
0x00f8 00248 (example.go:22) JMP 250
0x00fa 00250 (example.go:22) MOVQ AX, ""..autotmp_5+112(SP)
0x00ff 00255 (example.go:22) MOVQ $1, ""..autotmp_5+120(SP)
0x0108 00264 (example.go:22) MOVQ $1, ""..autotmp_5+128(SP)
0x0114 00276 (example.go:22) MOVQ AX, (SP)
0x0118 00280 (example.go:22) MOVQ $1, 8(SP)
0x0121 00289 (example.go:22) MOVQ $1, 16(SP)
0x012a 00298 (example.go:22) CALL fmt.Println(SB)
0x012f 00303 (example.go:23) MOVQ 160(SP), BP
0x0137 00311 (example.go:23) ADDQ $168, SP
0x013e 00318 (example.go:23) RET
0x013f 00319 (example.go:22) JMP 206
0x0141 00321 (example.go:22) NOP
0x0141 00321 (example.go:19) CALL runtime.morestack_noctxt(SB)
0x0146 00326 (example.go:19) JMP 0
最重要核心的代码是 runtime.convT2I(SB)
, 该函数位于 src/runtime/iface.go
当中. convT2I()
函数是将一个 struct
对象转换为 iface
. 类似的方法还有, convT64()
, convTstring()
等.这些方法都涉及到了接口的动态值.
下面看一下其中几个代表性的函数源码:
// 源码位置 src/runtime/iface.go
var (
uint64Eface interface{} = uint64InterfacePtr(0)
stringEface interface{} = stringInterfacePtr("")
sliceEface interface{} = sliceInterfacePtr(nil)
uint64Type *_type = (*eface)(unsafe.Pointer(&uint64Eface))._type
stringType *_type = (*eface)(unsafe.Pointer(&stringEface))._type
sliceType *_type = (*eface)(unsafe.Pointer(&sliceEface))._type
)
// type(uint64) -> interface
func convT64(val uint64) (x unsafe.Pointer) {
if val == 0 {
x = unsafe.Pointer(&zeroVal[0])
} else {
x = mallocgc(8, uint64Type, false)
*(*uint64)(x) = val
}
return
}
// type(uint64) -> interface
func convTstring(val string) (x unsafe.Pointer) {
if val == "" {
x = unsafe.Pointer(&zeroVal[0])
} else {
x = mallocgc(unsafe.Sizeof(val), stringType, true)
*(*string)(x) = val
}
return
}
// type -> iface
func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
t := tab._type
// 启用了 -race 选项
if raceenabled {
raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2I))
}
// 启用了 -msan 选项
if msanenabled {
msanread(elem, t.size)
}
// 生成 itab 当中动态类型的内存空间(指针), 并将 elem 的值拷贝相应位置
x := mallocgc(t.size, t, true) // convT2I 和 convT2Enoptr 的区别
typedmemmove(t, x, elem)
i.tab = tab
i.data = x
return
}
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer
分配一个 size 大小字节的 object. 从per-P缓存的空闲列表中分配小对象; 从堆直接分配大对象( >32 kB), 参数 needzero 描述分配的对象是否是指针 参数 typ 描述当前分配对象的数据类型
上述的代码逻辑都比较简单, 这里就不再解释了.
参考:
- 深度解密Go语言之关于 interface 的 10 个问题