介面定義了一種規範,描述了類的行為和功能。我們都知道,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。