「轉」Go Interface 源碼剖析

0.引言

在上一篇文章 《深入理解 Go Interface》中從設計和使用的角度介紹了 Golang 的 interface,作為補充,這篇文章將從源碼級別來看看 interface 的具體實現。所有源碼分析都是基於 Go 1.8.3。在開始之前,我們先看一個例子。

<code>func Foo(x interface{}) {
if x == nil {
fmt.Println("empty interface")
return
}
fmt.Println("non-empty interface")
}

func main() {
var x *int = nil
Foo(x)
}/<code>

「轉」Go Interface 源碼剖析


如果你對於上面的輸出結果有疑惑,那麼不妨帶著疑問來看這篇文章。上面的例子的輸出結果如下

<code>$ go run test_interface.go
non-empty interface/<code>

1. interface 底層結構

根據 interface 是否包含有 method,底層實現上用兩種 struct 來表示:iface 和 eface。eface表示不含 method 的 interface 結構,或者叫 empty interface。對於 Golang 中的大部分數據類型都可以抽象出來 _type 結構,同時針對不同的類型還會有一些其他信息。

iface 表示 non-empty interface 的底層實現。相比於 empty interface,non-empty 要包含一些 method。method 的具體實現存放在 itab.fun 變量裡。如果 interface 包含多個 method,這裡只有一個 fun 變量怎麼存呢?這個下面再細說。

<code>type eface struct {
_type *_type
data unsafe.Pointer
}

type _type struct {
size uintptr // type size
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32 // hash of type; avoids computation in hash tables
tflag tflag // extra type information flags
align uint8 // alignment of variable with this type
fieldalign uint8 // alignment of struct field with this type
kind uint8 // enumeration for C
alg *typeAlg // algorithm table
gcdata *byte // garbage collection data

str nameOff // string form
ptrToThis typeOff // type for pointer to this type, may be zero
}/<code>

<code>type iface struct {
tab *itab
data unsafe.Pointer
}

// layout of Itab known to compilers
// allocated in non-garbage-collected memory
// Needs to be in sync with
// ../cmd/compile/internal/gc/reflect.go:/^func.dumptypestructs.
type itab struct {
inter *interfacetype
_type *_type
link *itab
bad int32
inhash int32 // has this itab been added to hash?
fun [1]uintptr // variable sized
}/<code>


「轉」Go Interface 源碼剖析


我們使用實際程序來看一下。

<code> 1 package main
2
3 import (
4 "fmt"
5 )
6
7 type MyInterface interface {
8 Print()
9 }
10
11 type MyStruct struct{}
12 func (ms MyStruct) Print() {}
13
14 func main() {
15 x := 1
16 var y interface{} = x
17 var s MyStruct
18 var t MyInterface = s
19 fmt.Println(y, z)
20 }/<code>


查看彙編代碼。

<code>$ go build -gcflags '-l' -o interface11 interface11.go
$ go tool objdump -s "main\\.main" interface11
TEXT main.main(SB) /Users/kltao/code/go/examples/interface11.go
interface11.go:15 0x10870f0 65488b0c25a0080000 GS MOVQ GS:0x8a0, CX
interface11.go:15 0x10870f9 483b6110 CMPQ 0x10(CX), SP
interface11.go:15 0x10870fd 0f86de000000 JBE 0x10871e1
interface11.go:15 0x1087103 4883ec70 SUBQ $0x70, SP
interface11.go:15 0x1087107 48896c2468 MOVQ BP, 0x68(SP)
interface11.go:15 0x108710c 488d6c2468 LEAQ 0x68(SP), BP
interface11.go:17 0x1087111 48c744243001000000 MOVQ $0x1, 0x30(SP)
interface11.go:17 0x108711a 488d057fde0000 LEAQ 0xde7f(IP), AX
interface11.go:17 0x1087121 48890424 MOVQ AX, 0(SP)
interface11.go:17 0x1087125 488d442430 LEAQ 0x30(SP), AX
interface11.go:17 0x108712a 4889442408 MOVQ AX, 0x8(SP)
interface11.go:17 0x108712f e87c45f8ff CALL runtime.convT2E(SB)

interface11.go:17 0x1087134 488b442410 MOVQ 0x10(SP), AX
interface11.go:17 0x1087139 4889442438 MOVQ AX, 0x38(SP)
interface11.go:17 0x108713e 488b4c2418 MOVQ 0x18(SP), CX
interface11.go:17 0x1087143 48894c2440 MOVQ CX, 0x40(SP)
interface11.go:19 0x1087148 488d15b1000800 LEAQ 0x800b1(IP), DX
interface11.go:19 0x108714f 48891424 MOVQ DX, 0(SP)
interface11.go:19 0x1087153 488d542430 LEAQ 0x30(SP), DX
interface11.go:19 0x1087158 4889542408 MOVQ DX, 0x8(SP)
interface11.go:19 0x108715d e8fe45f8ff CALL runtime.convT2I(SB)/<code>

代碼 17 行 var y interface{} = x 調用了函數 runtime.convT2E ,將 int 類型的 x 轉換成 empty interface。代碼 19 行 var t MyInterface = s 將 MyStruct 類型轉換成 non-empty interface: MyInterface。

<code>func convT2E(t *_type, elem unsafe.Pointer) (e eface) {
...

x := newobject(t)
typedmemmove(t, x, elem)
e._type = t
e.data = x
return
}

func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
t := tab._type

...

x := newobject(t)
typedmemmove(t, x, elem)
i.tab = tab
i.data = x
return
}/<code>

看上面的函數原型,可以看出中間過程編譯器將根據我們的轉換目標類型的 empty interface 還是 non-empty interface,來對原數據類型進行轉換(轉換成 或者 )。這裡對於 struct 滿不滿足 interface 的類型要求(也就是 struct 是否實現了 interface 的所有 method),是由編譯器來檢測的。

2. itab

iface 結構中最重要的是 itab 結構。itab 可以理解為 pair\\ 。當然 itab 裡面還包含一些其他信息,比如 interface 裡面包含的 method 的具體實現。下面細說。itab 的結構如下。

<code>type itab struct {
inter *interfacetype
_type *_type
link *itab
bad int32
inhash int32 // has this itab been added to hash?
fun [1]uintptr // variable sized
}/<code>

其中 interfacetype 包含了一些關於 interface 本身的信息,比如 package path,包含的 method。上面提到的 iface 和 eface 是數據類型(built-in 和 type-define)轉換成 interface 之後的實體的 struct 結構,而這裡的 interfacetype 是我們定義 interface 時候的一種抽象表示。

<code>type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod
}

type imethod struct { //這裡的 method 只是一種函數聲明的抽象,比如 func Print() error
name nameOff
ityp typeOff
}/<code>

_type 表示 concrete type。fun 表示的 interface 裡面的 method 的具體實現。比如 interface type 包含了 method A, B,則通過 fun 就可以找到這兩個 method 的具體實現。這裡有個問題 fun 是長度為 1 的 uintptr 數組,那麼怎麼表示多個 method 呢?看一下測試程序。

<code>package main

type MyInterface interface {
Print()
Hello()
World()
AWK()
}

func Foo(me MyInterface) {
me.Print()
me.Hello()
me.World()
me.AWK()
}

type MyStruct struct {}

func (me MyStruct) Print() {}
func (me MyStruct) Hello() {}
func (me MyStruct) World() {}
func (me MyStruct) AWK() {}

func main() {
var me MyStruct
Foo(me)
}/<code>

看一下函數調用對應的彙編代碼。

<code>$ go build -gcflags '-l' -o interface8 interface8.go
$ go tool objdump -s "main\\.Foo" interface8
TEXT main.Foo(SB) /Users/kltao/code/go/examples/interface8.go
interface8.go:10 0x104c060 65488b0c25a0080000 GS MOVQ GS:0x8a0, CX
interface8.go:10 0x104c069 483b6110 CMPQ 0x10(CX), SP
interface8.go:10 0x104c06d 7668 JBE 0x104c0d7
interface8.go:10 0x104c06f 4883ec10 SUBQ $0x10, SP
interface8.go:10 0x104c073 48896c2408 MOVQ BP, 0x8(SP)
interface8.go:10 0x104c078 488d6c2408 LEAQ 0x8(SP), BP
interface8.go:11 0x104c07d 488b442418 MOVQ 0x18(SP), AX
interface8.go:11 0x104c082 488b4830 MOVQ 0x30(AX), CX //取得 Print 函數地址
interface8.go:11 0x104c086 488b542420 MOVQ 0x20(SP), DX
interface8.go:11 0x104c08b 48891424 MOVQ DX, 0(SP)
interface8.go:11 0x104c08f ffd1 CALL CX // 調用 Print()
interface8.go:12 0x104c091 488b442418 MOVQ 0x18(SP), AX

interface8.go:12 0x104c096 488b4828 MOVQ 0x28(AX), CX //取得 Hello 函數地址
interface8.go:12 0x104c09a 488b542420 MOVQ 0x20(SP), DX
interface8.go:12 0x104c09f 48891424 MOVQ DX, 0(SP)
interface8.go:12 0x104c0a3 ffd1 CALL CX //調用 Hello()
interface8.go:13 0x104c0a5 488b442418 MOVQ 0x18(SP), AX
interface8.go:13 0x104c0aa 488b4838 MOVQ 0x38(AX), CX //取得 World 函數地址
interface8.go:13 0x104c0ae 488b542420 MOVQ 0x20(SP), DX
interface8.go:13 0x104c0b3 48891424 MOVQ DX, 0(SP)
interface8.go:13 0x104c0b7 ffd1 CALL CX //調用 World()
interface8.go:14 0x104c0b9 488b442418 MOVQ 0x18(SP), AX
interface8.go:14 0x104c0be 488b4020 MOVQ 0x20(AX), AX //取得 AWK 函數地址
interface8.go:14 0x104c0c2 488b4c2420 MOVQ 0x20(SP), CX
interface8.go:14 0x104c0c7 48890c24 MOVQ CX, 0(SP)
interface8.go:14 0x104c0cb ffd0 CALL AX //調用 AWK()
interface8.go:15 0x104c0cd 488b6c2408 MOVQ 0x8(SP), BP
interface8.go:15 0x104c0d2 4883c410 ADDQ $0x10, SP
interface8.go:15 0x104c0d6 c3 RET
interface8.go:10 0x104c0d7 e8f48bffff CALL runtime.morestack_noctxt(SB)
interface8.go:10 0x104c0dc eb82 JMP main.Foo(SB)/<code>

其中 0x18(SP) 對應的 itab 的值。fun 在 x86-64 機器上對應 itab 內的地址偏移為 8+8+8+4+4 = 32 = 0x20,也就是 0x20(AX) 對應的 fun 的值,此時存放的 AWK 函數地址。然後 0x28(AX) = &Hello,0x30(AX) = &Print,0x38(AX) = &World。對的,每次函數是按字典序排序存放的。

我們再來看一下函數地址究竟是怎麼寫入的?首先 Golang 中的 uintptr 一般用來存放指針的值,這裡對應的就是函數指針的值(也就是函數的調用地址)。但是這裡的 fun 是一個長度為 1 的 uintptr 數組。我們看一下 runtime 包的 additab 函數。

<code>func additab(m *itab, locked, canfail bool) {
...
*(*unsafe.Pointer)(add(unsafe.Pointer(&m.fun[0]), uintptr(k)*sys.PtrSize)) = ifn
...
}/<code>

上面的代碼的意思是在 fun[0] 的地址後面依次寫入其他 method 對應的函數指針。熟悉 C++ 的同學可以類比 C++ 的虛函數表指針來看。

剩下的還有 bad,link,inhash。其中 bad 是一個表徵 itab 狀態的變量。而這裡的 link 是 *itab 類型,是不是表示 interface 的嵌套呢?並不是,interface 的嵌套也是把 method 平鋪而已。link 要和 inhash 一起來說。在 runtime 包裡面有一個 hash 表,通過 hash[hashitab(interface_type, concrete_type)] 可以取得 itab,這是出於性能方面的考慮。主要代碼如下,這裡就不再贅述了。

<code>const (
hashSize = 1009
)

var (
ifaceLock mutex // lock for accessing hash
hash [hashSize]*itab
)

func itabhash(inter *interfacetype, typ *_type) uint32 {
// compiler has provided some good hash codes for us.
h := inter.typ.hash
h += 17 * typ.hash
// TODO(rsc): h += 23 * x.mhash ?
return h % hashSize
}

func additab(...) {
...
h := itabhash(inter, typ)
m.link = hash[h]
m.inhash = 1
atomicstorep(unsafe.Pointer(&hash[h]), unsafe.Pointer(m))
}/<code>

3. Type Assertion

我們知道使用 interface type assertion (中文一般叫斷言) 的時候需要注意,不然很容易引入 panic。

<code>func do(v interface{}) {
n := v.(int) // might panic
}

func do(v interface{}) {
n, ok := v.(int)
if !ok {
// 斷言失敗處理
}
}/<code>

這個過程體現在下面的幾個函數上。

<code>// The assertXXX functions may fail (either panicking or returning false,
// depending on whether they are 1-result or 2-result).
func assertI2I(inter *interfacetype, i iface) (r iface) {
tab := i.tab
if tab == nil {
// explicit conversions require non-nil interface value.
panic(&TypeAssertionError{"", "", inter.typ.string(), ""})
}
if tab.inter == inter {
r.tab = tab
r.data = i.data
return
}
r.tab = getitab(inter, tab._type, false)
r.data = i.data
return
}
func assertI2I2(inter *interfacetype, i iface) (r iface, b bool) {
tab := i.tab
if tab == nil {
return
}
if tab.inter != inter {
tab = getitab(inter, tab._type, true)
if tab == nil {
return
}
}
r.tab = tab
r.data = i.data
b = true
return

}

// 類似
func assertE2I(inter *interfacetype, e eface) (r iface)
func assertE2I2(inter *interfacetype, e eface) (r iface, b bool)/<code>

「轉」Go Interface 源碼剖析


4. 總結

從某種意義上來說,Golang 的 interface 也是一種多態的體現。對比其他支持多態特性的語言,實現還是略有差異,很難說誰好誰壞。

5. 參考

  1. Go Data Structure: Interfaces (注:09 年的文章)
  2. A Quick Guide to Go’s Assembler
  3. runtime: need a better itab table #20505


分享到:


相關文章: