网站首页 > 教程文章 正文
Go语言interface的运行时实现的源码位于$GOROOT/src/runtime/runtime2.go中。 在Go的不同版本中,interface的实现可能会有不同,但整体结构变化不大,本文基于Go 1.17。
1.两类接口的运行时实现
可以在runtime/runtime2.go中找到Go的接口类型变量在运行时的表示,如下是iface和eface两个结构体:
// $GOROOT/src/runtime/runtime2.go
type iface struct {
tab *itab
data unsafe.Pointer
}
type eface struct {
_type *_type
data unsafe.Pointer
}
我们知道Go的interface两类,一类是拥有方法集(MethodSet)的接口;另一个类是没有方法的空接口,也就是interface{}。 iface和eface就分别在运行时表示这两个类接口类型的变量:
- iface - 表示拥有方法的接口类型变量
- eface - 表示m没有方法的空接口(empty interfac)类型变量,即interface{}类型的变量
1.1 iface struct
先看一下iface struct,它有两个指针字段tab和data。
// $GOROOT/src/runtime/runtime2.go
type iface struct {
tab *itab
data unsafe.Pointer
}
我们后边统一把实现了接口的类型成为具体类型。实际使用时,一般会把具体类型的变量赋值给接口类型变量。
data字段"指向"当前被赋值给接口类型变量的具体类型变量的值。
tab字段不仅被用来存储接口本身的信息(例如接口的类型信息、方法集信息等),还被用来存储具体类型所实现的信息。tab字段是一个itab struct的指针。 itab这个struct的定义如下:
// $GOROOT/src/runtime/runtime2.go
type itab struct {
inter *interfacetype
_type *_type
hash uint32 // copy of _type.hash. Used for type switches.
_ [4]byte
fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
itab的inter字段存储就是接口本身的信息。inter字段是一个interfacetype struct的指针,它的定义如下:
// $GOROOT/src/runtime/runtime2.go
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod
}
可以看到interfacetype定义中包含了接口类型typ, 包路径名pkgpath和用来存储接口方法集的切片mhdr。
我们回到itab结构体定义,上面学习inter字段存储的是接口本身的信息(接口类型、方法集等),那么剩下的_type和fun被分别用来存储具体类型信息和具体类型实现了接口方法的调用地址:
- _type是具体的具体类型信息
- fun存储一组函数指针,是一个用于动态分发的虚函数表
- hash字段是_type.hash的缓存,当需要将接口类型转换成具体的类型时,使用该字段判断转换的目标类型是否和具体类型_type一样
上面分析了iface struct的定义,总结如下:
- iface用来在运行时表示拥有方法的接口类型的变量
- iface内部有两个指针字段: tab和data。data"指向"当前被赋值给接口类型变量的具体类型变量值,tab存储了接口类型信息、接口方法信息、具体类型信息及具体类型信息实现接口方法的调用地址表
例1:
package main
import "fmt"
type Flyable interface {
Fly()
}
type Bird struct {
}
func (b *Bird) Fly() {
fmt.Println("Bird fly.")
}
func main() {
var f Flyable
println(f, f == nil) // (0x0,0x0) true
var bird *Bird
f = bird
println(f, f == nil) // (0x10991e0,0x0) false
bird = &Bird{}
f = bird
println(f, f == nil) // (0x1099220,0xc00005ef70) false
}
我们编写上面例1的代码,并使用delve调试工具在第18行println函数出加断点调试,将代码执行到第18行断点处:
dlv debug
Type 'help' for list of commands.
(dlv) b main.go:18
Breakpoint 1 set at 0x107f87e for main.main() ./main.go:18
(dlv) c
> main.main() ./main.go:18 (hits goroutine(1):1 total:1) (PC: 0x107f87e)
13: fmt.Println("Bird fly.")
14: }
15:
16: func main() {
17: var f Flyable
=> 18: println(f, f == nil) // (0x0,0x0) true
19:
20: var bird *Bird
21: f = bird
22: println(f, f == nil) // (0x10991e0,0x0) false
23:
(dlv)
使用disass命令查看这段代码的反汇编:
(dlv) disass
TEXT main.main(SB) /Users/Erich/Workspace/go-showcase/main.go
main.go:16 0x107f860 493b6610 cmp rsp, qword ptr [r14+0x10]
main.go:16 0x107f864 0f860f010000 jbe 0x107f979
main.go:16 0x107f86a 4883ec48 sub rsp, 0x48
main.go:16 0x107f86e 48896c2440 mov qword ptr [rsp+0x40], rbp
main.go:16 0x107f873 488d6c2440 lea rbp, ptr [rsp+0x40]
main.go:17 0x107f878 440f117c2430 movups xmmword ptr [rsp+0x30], xmm15
=> main.go:18 0x107f87e* c644241701 mov byte ptr [rsp+0x17], 0x1
main.go:18 0x107f883 e8d822fbff call $runtime.printlock
main.go:18 0x107f888 488b442430 mov rax, qword ptr [rsp+0x30]
main.go:18 0x107f88d 488b5c2438 mov rbx, qword ptr [rsp+0x38]
main.go:18 0x107f892 e8092dfbff call $runtime.printiface
main.go:18 0x107f897 e8e424fbff call $runtime.printsp
main.go:18 0x107f89c 0fb6442417 movzx eax, byte ptr [rsp+0x17]
main.go:18 0x107f8a1 e85a25fbff call $runtime.printbool
main.go:18 0x107f8a6 e81525fbff call $runtime.printnl
main.go:18 0x107f8ab e83023fbff call $runtime.printunlock
......
可以看到第18行会调用runtime.printiface函数,在runtime.printiface上打个断点,并执行到该断点处:
(dlv) b runtime.printiface
Breakpoint 2 set at 0x10325a6 for runtime.printiface() /usr/local/Cellar/go/1.17.3/libexec/src/runtime/print.go:260
(dlv) c
> runtime.printiface() /usr/local/Cellar/go/1.17.3/libexec/src/runtime/print.go:260 (hits goroutine(1):1 total:1) (PC: 0x10325a6)
Warning: debugging optimized function
255:
256: func printeface(e eface) {
257: print("(", e._type, ",", e.data, ")")
258: }
259:
=> 260: func printiface(i iface) {
261: print("(", i.tab, ",", i.data, ")")
262: }
263:
(dlv)
可以看到边以及将println(f)替换成了runtime.printiface。runtime.printiface的实现相当简单,就是但因了iface结构体中的tab和data字段。
学习了runtime.printiface函数之后,再看一下例1的代码:
- 第17行初始化了一个Flyable接口的变量f,注意这个接口的方法集不为空,因此它在运行时表示为iface。
- 第18行使用println(f, f == nil)打印f,此时f还未被赋值,打印结果为(0x0,0x0) true。即f在运行时iface表示中的tab和data指针字段都是0x0(空的),因此f == nil 为true。
- 第18行说明了只有iface为(0x0, 0x0)时,它才能和nil划等号。
- 第20行声明了一个Bird的结构体指针bird变量, bird的值为nil,bird的类型为*Bird
- 第21行将具体类型变量bird赋值给接口变量f时,f的运行时表示iface中的data将会被赋值为具体类型变量的值nil,因此data的值将会是0x0,而tab中会存储具体类型和接口类型的信息后就不再是0x0了。因此第22行println(f, f == nil)打印结果是(0x10991e0,0x0) false。此时虽然iface中的data为空(0x0),但tab不再为空(0x10991e0),所以f == nil为false。
- 第24行为具体类型指针变量bird分配了内存,bird不再为nil
- 第25行将bird赋值给f,此时f的运行时表示iface中的tab和data都不为空,因此第26行打印结果是(0x1099220,0xc00005ef70) false,f == nil为false。
1.2 eface struct
前面学习了iface后,再学eface就十分简单了。
eface的定义如下:
// $GOROOT/src/runtime/runtime2.go
type eface struct {
_type *_type
data unsafe.Pointer
}
- eface用来在运行时表示方法集为空的接口类型变量,如interface{}类型。
- eface结构体有两个指针类型的字段_type和data。_type表示具体类型的类型信息,data执行具体类型变量的值。
例2:
package main
import "fmt"
type Bird struct {
}
func (b *Bird) Fly() {
fmt.Println("Bird fly.")
}
func main() {
var f interface{}
println(f, f == nil) // (0x0,0x0) true
var bird *Bird
f = bird
println(f, f == nil) // (0x1074f40,0x0) false
bird = &Bird{}
f = bird
println(f, f == nil) // (0x1074f40,0xc000092f70) false
}
例2中println(f) println一个eface变量,将会被编译器替换成runtime.printeface。
// $GOROOT/src/runtime/print.go
func printeface(e eface) {
print("(", e._type, ",", e.data, ")")
}
- 第13行初始化了一个interface{}接口的变量f,注意这个接口的方法集为空,因此它在运行时表示为eface。
- 第14行使用println(f, f == nil)打印f,此时f还未被赋值,打印结果为(0x0,0x0) true。即f在运行时eface表示中的_type和data指针字段都是0x0(空的),因此f == nil 为true。
- 第14行说明了只有eface为(0x0, 0x0)时,它才能和nil划等号。
- 第16行声明了一个Bird的结构体指针bird变量, bird的值为nil,bird的类型为*Bird
- 第17行将具体类型变量bird赋值给接口变量f时,f的运行时表示eface中的data将会被赋值为具体类型变量的值nil,因此data的值将会是0x0,而_type中存储具体类型信息后就不再是0x0了。因此第18行println(f, f == nil)打印结果是(0x1074f40,0x0) false。此时虽然eface中的data为空(0x0),但_type不再为空(0x1074f40),所以f == nil为false。
- 第20行为具体类型指针变量bird分配了内存,bird不再为nil
- 第21行将bird赋值给f,此时f的运行时表示eface中的_type和data都不为空,因此第22行打印结果是(0x1074f40,0xc000092f70) false,f == nil为false。
1.3 iface和eface示意图
下面根据前面1.1和1.2学习的内容,当将一个具体类型的变量赋给接口类型的变量时,整理绘制了如下的iface和eface在运行时表示的示意图。
当将一个具体类型的变量赋值给一个方法集不为空的接口类型的变量时,会创建一个iface结构体,iface结构体中的tab字段指向接口类型信息和具体类型信息,iface结构体的data字段"指向"具体类型的变量值。
当将一个具体类型的变量赋值给一个方法集为空的接口类型的变量时(例如interface{}),会创建一个eface结构体,eface结构体的_type字段是具体类型信息,eface结构体的data字段"指向"具体类型的变量值。
因此,将一个具体类型变量赋值给一个具体类型的变量的操作,会发生iface或eface的创建操作,这个操作可以理解为是一种装箱操作(Boxing),即将具体类型的data装箱为iface或eface。
2.理解Go interface的装箱操作
前面提到,当将具体类型变量的值赋值给接口类型的变量时,会进行iface或eface的装箱操作,iface或eface的data字段会"指向"具体类型的变量的值。 这里这个"指向"我们加了引号,需要好好理解它。
不管是iface还是eface,它们的data字段都是一个unsafe.Pointer类型的指针,那这就面临以下几个问题:
- 如果具体类型的值是值类型的话,那么在装箱操作时,是直接将值的地址直接赋值给data这个指针吗?还是会拷贝一份具体类型的值,将拷贝的值的地址赋值为data这个指针?
- 如果具体类型的值是指针类型的话,那么在装箱操作时,是直接将这个指针值赋值给data吗? 还是会拷贝一份具体类型指向的值,将拷贝的值的地址赋值为data这个指针?
可以从下面例3和例4两个例子中找到答案。
例3:
package main
import "fmt"
type Bird struct {
name string
color int
}
func (b Bird) Fly() {
}
type Flyable interface {
Fly()
}
func main() {
bird := Bird{name: "a", color: 100}
var efc interface{} = bird // eface boxing
var ifc Flyable = bird // iface boxing
fmt.Printf("bird=%+v, efc=%+v, ifc=%+v\n", bird, efc, ifc) // bird={name:a color:100}, efc={name:a color:100}, ifc={name:a color:100}
println(&bird, efc, ifc) // 0xc00005eee0 (0x109a160,0xc00000c030) (0x10c1620,0xc00000c048)
bird.name = "b"
fmt.Printf("bird=%+v, efc=%+v, ifc=%+v\n", bird, efc, ifc) // bird={name:b color:100}, efc={name:a color:100}, ifc={name:a color:100}
println(&bird, efc, ifc) // 0xc00005eee0 (0x109a160,0xc00000c030) (0x10c1620,0xc00000c048)
}
例3中将结构体bird这个值类型进行装箱操作为eface和iface时,从打印结果可以看出, bird的地址是0xc00005eee0,efc中data的值是0xc00000c030, ifc中data的值是0xc00000c048。 efc和ifc中的data没有指向具体类型的值,efc和ifc应该是各自都将具体类型的值拷贝了一份,然后指向了拷贝的值。为了验证这个问题,还是祭出dlv + disass反汇编大法查看一下汇编代码,当然也可以使用go tool compile -S,只是我用惯了前者。
dlv debug
Type 'help' for list of commands.
(dlv) b main.go:19
Breakpoint 1 set at 0x10ad885 for main.main() ./main.go:19
(dlv) c
> main.main() ./main.go:19 (hits goroutine(1):1 total:1) (PC: 0x10ad885)
14: Fly()
15: }
16:
17: func main() {
18: bird := Bird{name: "a", color: 100}
=> 19: var efc interface{} = bird // eface boxing
20: var ifc Flyable = bird // iface boxing
21:
22: fmt.Printf("bird=%+v, efc=%+v, ifc=%+v\n", bird, efc, ifc) // bird={name:a color:100}, efc={name:a color:100}, ifc={name:a color:100}
23: println(&bird, efc, ifc) // 0xc00005eee0 (0x109a160,0xc00000c030) (0x10c1620,0xc00000c048)
24: bird.name = "b"
(dlv) disass
TEXT main.main(SB) /Users/Erich/Workspace/go-showcase/main.go
main.go:17 0x10ad820 4c8da42450ffffff lea r12, ptr [rsp+0xffffff50]
main.go:17 0x10ad828 4d3b6610 cmp r12, qword ptr [r14+0x10]
main.go:17 0x10ad82c 0f8690040000 jbe 0x10adcc2
main.go:17 0x10ad832 4881ec30010000 sub rsp, 0x130
main.go:17 0x10ad839 4889ac2428010000 mov qword ptr [rsp+0x128], rbp
main.go:17 0x10ad841 488dac2428010000 lea rbp, ptr [rsp+0x128]
main.go:18 0x10ad849 440f11bc2498000000 movups xmmword ptr [rsp+0x98], xmm15
main.go:18 0x10ad852 48c78424a800000000000000 mov qword ptr [rsp+0xa8], 0x0
main.go:18 0x10ad85e 488d0d33830100 lea rcx, ptr [rip+0x18333]
main.go:18 0x10ad865 48898c2498000000 mov qword ptr [rsp+0x98], rcx
main.go:18 0x10ad86d 48c78424a000000001000000 mov qword ptr [rsp+0xa0], 0x1
main.go:18 0x10ad879 48c78424a800000064000000 mov qword ptr [rsp+0xa8], 0x64
=> main.go:19 0x10ad885* 48898c24c8000000 mov qword ptr [rsp+0xc8], rcx
main.go:19 0x10ad88d 48c78424d000000001000000 mov qword ptr [rsp+0xd0], 0x1
main.go:19 0x10ad899 48c78424d800000064000000 mov qword ptr [rsp+0xd8], 0x64
main.go:19 0x10ad8a5 488d05d4ed0000 lea rax, ptr [rip+0xedd4]
main.go:19 0x10ad8ac 488d9c24c8000000 lea rbx, ptr [rsp+0xc8]
main.go:19 0x10ad8b4 e847c0f5ff call $runtime.convT2E
main.go:19 0x10ad8b9 4889442468 mov qword ptr [rsp+0x68], rax
main.go:19 0x10ad8be 48895c2470 mov qword ptr [rsp+0x70], rbx
main.go:20 0x10ad8c3 488b8c2498000000 mov rcx, qword ptr [rsp+0x98]
main.go:20 0x10ad8cb 488b9424a0000000 mov rdx, qword ptr [rsp+0xa0]
main.go:20 0x10ad8d3 488bb424a8000000 mov rsi, qword ptr [rsp+0xa8]
main.go:20 0x10ad8db 48898c24c8000000 mov qword ptr [rsp+0xc8], rcx
main.go:20 0x10ad8e3 48899424d0000000 mov qword ptr [rsp+0xd0], rdx
main.go:20 0x10ad8eb 4889b424d8000000 mov qword ptr [rsp+0xd8], rsi
main.go:20 0x10ad8f3 488d059e480200 lea rax, ptr [rip+0x2489e]
main.go:20 0x10ad8fa 488d9c24c8000000 lea rbx, ptr [rsp+0xc8]
main.go:20 0x10ad902 e8d9c2f5ff call $runtime.convT2I
main.go:20 0x10ad907 4889442458 mov qword ptr [rsp+0x58], rax
main.go:20 0x10ad90c 48895c2460 mov qword ptr [rsp+0x60], rbx
main.go:22 0x10ad911 488b8c2498000000 mov rcx, qword ptr [rsp+0x98]
例3代码的第19行和第20行的装箱操作,从汇编代码中找到了对应位置发现调用了runtime.convT2E和runtime.convT2I两个函数。进一步调试进去查看这两个函数的内容:
(dlv) b runtime.convT2E
Breakpoint 2 set at 0x1009906 for runtime.convT2E() /usr/local/Cellar/go/1.17.3/libexec/src/runtime/iface.go:318
(dlv) b runtime.convT2I
Breakpoint 3 set at 0x1009be6 for runtime.convT2I() /usr/local/Cellar/go/1.17.3/libexec/src/runtime/iface.go:405
在$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)
// TODO: We allocate a zeroed object only to overwrite it with actual data.
// Figure out how to avoid zeroing. Also below in convT2Eslice, convT2I, convT2Islice.
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
}
这两个函数中的x变量都是使用mallocgc重新分配的内存,然后拷贝了具体类型的值。
例4:
package main
import "fmt"
type Bird struct {
name string
color int
}
func (b *Bird) Fly() {
}
type Flyable interface {
Fly()
}
func main() {
bird := &Bird{name: "a", color: 100}
var efc interface{} = bird // eface boxing
var ifc Flyable = bird // iface boxing
fmt.Printf("bird=%+v, efc=%+v, ifc=%+v\n", bird, efc, ifc) // bird=&{name:a color:100}, efc=&{name:a color:100}, ifc=&{name:a color:100}
println(bird, efc, ifc) // 0xc00000c030 (0x10954e0,0xc00000c030) (0x10c1420,0xc00000c030)
bird.name = "b"
fmt.Printf("bird=%+v, efc=%+v, ifc=%+v\n", bird, efc, ifc) // bird=&{name:b color:100}, efc=&{name:b color:100}, ifc=&{name:b color:100}
println(bird, efc, ifc) // 0xc00000c030 (0x10954e0,0xc00000c030) (0x10c1420,0xc00000c030)
}
例4中将结构体指针bird这个指针类型进行装箱操作为eface和iface时,从打印结果可以看出, bird的地址, efc中data的值, ifc中data的值,三者都是0xc00000c030。 efc和ifc中的data直接使用了被装箱的指针类型值。此时如果祭出dlv + disass大法查看汇编代码的话,因为例4的装箱操作不再涉及值的拷贝,所以不会再调用runtime.convT2E和runtime.convT2I。
另外,需要注意在$GOROOT/src/runtime/iface.go中还有很多convXXX函数,这些都是Go为了不同类型值赋值给接口变量进行装箱操作的实现。例如包含convTslice, convTstring等,因为装箱是一个有性能损耗的操作,从这些函数的注释上可以看出Go本身不断在对这块进行优化。
参考
- https://go101.org/article/interface.html
- 上一篇: 如何debug一个正在运行的go进程
- 下一篇: go语言中关于内存分配详解
猜你喜欢
- 2025-01-05 OpenShift 平台企业版 OCP 4.11.9 部署(基于KVM,CentOS, CoreOS)
- 2025-01-05 春节消费靠Z世代?这10个问题我们准备好了
- 2025-01-05 我们在战位,向祖国母亲献礼!
- 2025-01-05 WLK怀旧服WA:猎人核心输出技能循环
- 2025-01-05 K8s里我的容器到底用了多少内存?
- 2025-01-05 AndroidStudio_Android使用OkHttp发起Http请求
- 2025-01-05 魔兽一秒学会惩戒骑:打地鼠WA
- 2025-01-05 魔兽世界WLK德鲁伊通用技能提示
- 2025-01-05 Windows常用的一些CMD运行命令
- 2025-01-05 服务部署 - DNS域名解析服务配置
- 最近发表
- 标签列表
-
- location.href (44)
- document.ready (36)
- git checkout -b (34)
- 跃点数 (35)
- 阿里云镜像地址 (33)
- qt qmessagebox (36)
- mybatis plus page (35)
- vue @scroll (38)
- 堆栈区别 (33)
- 什么是容器 (33)
- sha1 md5 (33)
- navicat导出数据 (34)
- 阿里云acp考试 (33)
- 阿里云 nacos (34)
- redhat官网下载镜像 (36)
- srs服务器 (33)
- pico开发者 (33)
- https的端口号 (34)
- vscode更改主题 (35)
- 阿里云资源池 (34)
- os.path.join (33)
- redis aof rdb 区别 (33)
- 302跳转 (33)
- http method (35)
- js array splice (33)