管中窥 Go: interface - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
GopherDaily
V2EX    Go 编程语言

管中窥 Go: interface

  •  2
     
  •   GopherDaily 2024-05-28 01:37:10 +08:00 2451 次点击
    这是一个创建于 501 天前的主题,其中的信息可能已经有所发展或是发生改变。

    如未做特别说明, 全文依然假设 GOOS=linux GOARCH=amd64, go 的版本为 go1.21.8.

    值调用指针方法的实现

    这大概率是一个语法糖, 由编译器塞入取地址指令. 我们构造了如下的例子, 编译后通过汇编来验证我们的猜想.

     1 package main 2 3 type Foo struct { 4 name string 5 age int 6 } 7 8 //go:noinline 9 func (foo *Foo) ChangeName(name string) { 10 foo.name = name 11 } 12 13 func main() { 14 foo := Foo{name: "foo", age: 35} 15 foo.ChangeName("bar") 16 (&foo).ChangeName("baz") 17 } 
     go-generic git:(main) GOOS=linux GOARCH=amd64 go build value_call_ptr_method.go go-generic git:(main) GOOS=linux GOARCH=amd64 go tool objdump -S value_call_ptr_method | grep "foo.ChangeName(\"bar" -A 11 foo.ChangeName("bar") 0x45786c 488d442418 LEAQ 0x18(SP), AX 0x457871 488d1d9dea0000 LEAQ 0xea9d(IP), BX 0x457878 b903000000 MOVL $0x3, CX 0x45787d 0f1f00 NOPL 0(AX) 0x457880 e85bffffff CALL main.(*Foo).ChangeName(SB) (&foo).ChangeName("baz") 0x457885 488d442418 LEAQ 0x18(SP), AX 0x45788a 488d1d87ea0000 LEAQ 0xea87(IP), BX 0x457891 b903000000 MOVL $0x3, CX 0x457896 e845ffffff CALL main.(*Foo).ChangeName(SB) } 

    我们可以看到两次调用的汇编几乎是一致, 其中:

    • 0x45786c 将 foo 的地址加载到寄存器 AX, 调用方法时, 需要将接受者作为一个参数传入.
    • 0x457871 和 0x457878 将字符串 bar 加载到寄存器 BX, CX, 字符串需要用连个寄存器
    • 0x45787d 是 NOPL 指令, 没有实际影响
    • 0x457880 调用 ChangeName, 两个参数依次被保存在 AX, BX+CX

    interface 的实现

    Russ Cox 的 Go Data Structures: Interfaces 是了解 interface 实现的最好入口之一. 在此基础上, 我们通过一些构造的例子来加深/验证自己的理解.

     1 package main 2 3 import ( 4 "unsafe" 5 ) 6 7 type Namer interface { 8 GetName() string 9 } 10 11 type User struct { 12 Name string 13 Age int 14 } 15 16 func (u User) GetName() string { return u.Name } 17 18 //go:noinline 19 func getName(i Namer) string { return i.GetName() } 20 21 func main() { 22 u := User{"Foo", 35} 23 i := Namer(u) 24 getName(i) 25 } 

    上述代码对应的汇编如下:

     go-generic git:(main) GOOS=linux GOARCH=amd64 go tool compile -S itf.go | sed 's/\/Users\/j2gg0s\/go\/src\/github.com\/j2gg0s\/j2gg0s\/examples\/go-generic\///g' | cat -n - | grep -E "itf.go:(22|23|24)" 65 0x000e 00014 (itf.go:22) MOVQ $0, main.u+16(SP) 66 0x0017 00023 (itf.go:22) MOVUPS X15, main.u+24(SP) 67 0x001d 00029 (itf.go:22) LEAQ go:string."Foo"(SB), CX 68 0x0024 00036 (itf.go:22) MOVQ CX, main.u+16(SP) 69 0x0029 00041 (itf.go:22) MOVQ $3, main.u+24(SP) 70 0x0032 00050 (itf.go:22) MOVQ $35, main.u+32(SP) 71 0x003b 00059 (itf.go:23) LEAQ type:<unlinkable>.User(SB), AX 72 0x0042 00066 (itf.go:23) LEAQ main.u+16(SP), BX 73 0x0047 00071 (itf.go:23) PCDATA $1, $0 74 0x0047 00071 (itf.go:23) CALL runtime.convT(SB) 75 0x004c 00076 (itf.go:24) MOVQ AX, BX 76 0x004f 00079 (itf.go:24) LEAQ go:itab.<unlinkable>.User,<unlinkable>.Namer(SB), AX 77 0x0056 00086 (itf.go:24) CALL main.getName(SB) 

    其中:

    • L67~L70 新建了变量 u 并存放在 16(SP)
    • L71 将类型 User 的加载到寄存器 AX
    • L72 将变量 u 的地址加载到寄存器 BX
    • L74 调用 runtime.convT, 两个入参保存在 AX 和 BX
    • L75 将 runtime.convT 的返回从寄存器 AX 移动到寄存器 BX
    • L76 将 interface 的 itab 加载到寄存器 AX
    • L77 调用 main.getName

    结合上述的汇编代码和 runtime, 不难理解 interface 在 runtime 中对应的结构体 iface.

    type iface struct { tab *itab data unsafe.Pointer } ... 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. } 

    直观的来看, iface 保存的核心信息是:

    • interface 的类型, itab.inter
    • 底层的精确类型, itab._type
    • 底层的值, data

    L76 的 go:itab.User,.Namer 大概率是编译器结合 interface 和 struct 构造的 itab, 但是 go tool compile 并没有直接给出可以验证这一点的内容. 我们参考 go-internal , 尝试从 elf 文件中读取读取相关内容.

     go-generic git:(main) GOOS=linux GOARCH=amd64 go build itf.go go-generic git:(main) go-generic git:(main) x86_64-linux-gnu-objdump -t -j .rodata itf | grep Namer 000000000047e3a8 g O .rodata 0000000000000020 go:itab.main.User,main.Namer go-generic git:(main) x86_64-linux-gnu-objdump -t -j .rodata itf | grep go:itab.main.User,main.Namer | awk '{print "ibase=16;"toupper($1)}' | bc 4711336 go-generic git:(main) x86_64-linux-gnu-objdump -t -j .rodata itf | grep go:itab.main.User,main.Namer | awk '{print "ibase=16;"toupper($5)}' | bc 32 go-generic git:(main) go-generic git:(main) x86_64-linux-gnu-readelf -St -W itf | grep -A 1 .rodata | tail -n +2 PROGBITS 0000000000458000 058000 0272c6 00 0 0 32 go-generic git:(main) x86_64-linux-gnu-readelf -St -W itf | grep -A 1 .rodata | tail -n +2 | awk '{print "ibase=16;"toupper($3)}' | bc 360448 go-generic git:(main) x86_64-linux-gnu-readelf -St -W itf | grep -A 1 .rodata | tail -n +2 | awk '{print "ibase=16;"toupper($2)}' | bc 4554752 
    1. 我们首先将代码编译到指定平台
    2. 随后读取到 go:itab.main.User,main.Namer 的地址和长度: 4711336(0x47e3a8) 和 32(0x20).
    3. 为了将地址转换到 elf 文件内的偏移量, 读取 .rodata 的偏移量和地址: 360448(0x58000) 和 4554752(0x458000)

    所以 go:itab.main.User,main.Namer 应该在文件的第 4711336-4554752+360448=517032 开始的 32 个字节.

     go-generic git:(main) dd if=itf of=/dev/stdout bs=1 count=32 skip=517032 2>/dev/null | hexdump 0000000 e920 0045 0000 0000 1160 0046 0000 0000 0000010 2a0b 99d4 0000 0000 7940 0045 0000 0000 0000020 

    通过 itab.hash 可以验证上述数据的准确性:

     22 func main() { 23 u := User{"Foo", 35} 24 i := Namer(u) 25 getName(i) 26 27 iface := (*iface)(unsafe.Pointer(&i)) 28 fmt.Printf("%#x\n", iface.tab.hash) 29 } 30 31 // simplified definitions of runtime's iface & itab types 32 type iface struct { 33 tab *struct { 34 inter uintptr 35 _type uintptr 36 hash uint32 37 _ [4]byte 38 fun [1]uintptr 39 } 40 data unsafe.Pointer 41 } 

    运行结果 0x99d42a0b 和第 24 到 32 字节的内容完全相符.

    iterface 的代价

    比较直观的一点是, 在通过 runtime.convT 将值转换为 iface.data 时, 可能需要分配一个堆上对象.

     go-generic git:(main) cat -n itf_test.go 1 package main 2 3 import ( 4 "fmt" 5 "io" 6 "testing" 7 ) 8 9 func BenchmarkInterfaceAllocate(b *testing.B) { 10 u := User{"Foo", 35} 11 12 var v interface{} 13 for i := 0; i < b.N; i++ { 14 v = interface{}(u) 15 } 16 17 fmt.Fprintln(io.Discard, v) 18 } 19 20 func BenchmarkInterfaceNoAllocate(b *testing.B) { 21 var v interface{} 22 for i := 0; i < b.N; i++ { 23 v = interface{}(32) 24 } 25 26 fmt.Fprintln(io.Discard, v) 27 } 28 29 func BenchmarkInterfacePtr(b *testing.B) { 30 u := &User{"Foo", 35} 31 32 var v interface{} 33 for i := 0; i < b.N; i++ { 34 v = interface{}(u) 35 } 36 37 fmt.Fprintln(io.Discard, v) 38 } go-generic git:(main) go test -benchmem --bench=. ./... goos: darwin goarch: arm64 pkg: github.com/j2gg0s/j2gg0s/examples/go-generic BenchmarkInterfaceAllocate-10 62783398 19.01 ns/op 24 B/op 1 allocs/op BenchmarkInterfaceNoAllocate-10 1000000000 0.2959 ns/op 0 B/op 0 allocs/op BenchmarkInterfacePtr-10 1000000000 0.2911 ns/op 0 B/op 0 allocs/op PASS ok github.com/j2gg0s/j2gg0s/examples/go-generic 2.834s 

    复杂的是 interface 的 method dispatch. 从 Russ Cox 的文章中, 我们可以理解转发表保存在 itab.fun, 并由 runtime 在运行时构建. 但是从之前的例子来看, 依然存在的困惑点:

    • 有谁触发并构建了 itab.fun
    • 调用 main.getName 时并不能看到相关逻辑, 是编译器针对此类 case 直接填充了?

    针对前一个问题, 我们构建一个 interface2interface 的例子来触发相关逻辑.

     go-generic git:(main) cat -n i2i.go 1 package main 2 3 type Namer interface { 4 GetName() string 5 } 6 7 type NamerAndAger interface { 8 GetName() string 9 GetAge() int 10 } 11 12 type User struct { 13 Name string 14 Age int 15 } 16 17 func (u User) GetName() string { return u.Name } 18 func (u User) GetAge() int { return u.Age } 19 20 //go:noinline 21 func getName(i Namer) string { return i.GetName() } 22 23 func main() { 24 u := NamerAndAger(User{"Foo", 35}) 25 getName(u) 26 } go-generic git:(main) GOOS=linux GOARCH=amd64 go tool compile -S i2i.go | sed 's/\/Users\/j2gg0s\/go\/src\/github.com\/j2gg0s\/j2gg0s\/examples\/go-generic\///g' | cat -n - | grep "i2i.go:25" 87 0x0051 00081 (i2i.go:25) LEAQ go:itab.<unlinkable>.User,<unlinkable>.NamerAndAger(SB), BX 88 0x0058 00088 (i2i.go:25) LEAQ type:<unlinkable>.Namer(SB), AX 89 0x005f 00095 (i2i.go:25) PCDATA $1, $1 90 0x005f 00095 (i2i.go:25) NOP 91 0x0060 00096 (i2i.go:25) CALL runtime.convI2I(SB) 92 0x0065 00101 (i2i.go:25) MOVQ main..autotmp_7+16(SP), BX 93 0x006a 00106 (i2i.go:25) PCDATA $1, $0 94 0x006a 00106 (i2i.go:25) CALL main.getName(SB) 

    此时, 我们不再调用 convT, 而是调用 runtime.convI2I. 其内部会调用 itab.init 构建 itab.fun.

    对于后一个问题, 我们依然可以通过读取 elf 中的内容来验证. 回顾 go.itab.main.User,main.Namer 其值, 除去用于对其的 4 个字节, fun 的值是 7940 0045, 对应偏移为 0x457940. 毫不意外, 其是 User.GetName 的入口.

     go-generic git:(main) dd if=itf of=/dev/stdout bs=1 count=32 skip=517032 2>/dev/null | hexdump 0000000 e920 0045 0000 0000 1160 0046 0000 0000 0000010 2a0b 99d4 0000 0000 7940 0045 0000 0000 0000020 go-generic git:(main) x86_64-linux-gnu-objdump -t -j .text itf | grep 457940 0000000000457940 g F .text 0000000000000037 main.(*User).GetName 
    4 条回复    2024-05-28 18:58:29 +08:00
    billzhuang
        1
    billzhuang  
       2024-05-28 09:34:50 +08:00
    漂亮!
    6HWcp545hm0RHi6G
        2
    6HWcp545hm0RHi6G  
       2024-05-28 09:36:27 +08:00
    学到了学到了, 比起枯燥的文字描述,我更喜欢看交互细节。
    nextvay
        3
    nextvay  
       2024-05-28 18:21:38 +08:00
    太难了
    GopherDaily
        4
    GopherDaily  
    OP
       2024-05-28 18:58:29 +08:00
    @nextvay

    asm 习惯了就好,Go Team 还是比较愿意写文章的。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2738 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 35ms UTC 11:38 PVG 19:38 LAX 04:38 JFK 07:38
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86