Golang 接口原理

問題

小提示, 若想直接查看原理, 可從接口原理開始查看.

有這樣一段GO代碼:

func main() {
	var obj interface{}
	fmt.Printf("obj == nil. %b\n", obj == nil)
	type st struct{}
	var s *st
	obj = s
	fmt.Printf("s == nil. %b\n", s == nil)
	fmt.Printf("obj == nil. %b\n", obj == nil)
}

先盲猜一下結果.

  1. 第一次nil的判斷, 結果爲 true, 沒什麼疑問吧
  2. 第二次判斷, s爲空指針, 結果爲true
  3. 第三次判斷, objs相等, 故也爲空指針, 結果爲true.

如果你也是這麼認爲, 那麼結果會令你像我一樣十分驚訝:

image-20220802203427661

???第三次判斷, obj不爲nil???意不意外? 驚不驚喜? 刺不刺激? 爲什麼會發生這樣的事情呢?

搭建 gdb 調試環境

爲了知道爲什麼發生這種問題, 我嘗試了各種方式, 斷點調試, 查看彙編內容等等, 最終發現, 通過gdb工具查看十分方便.

在這之前, 先簡單介紹一下gdb調試環境的使用. 不感興趣可直接跳過

爲了方便, 直接使用docker鏡像了. 這裏我使用的鏡像爲: golang:1.18 其他版本大同小異. 這裏直接上結論了, 中間踩坑過程不再贅述.

# 安裝 gdb 工具
apt update && apt install -y gdb
echo 'add-auto-load-safe-path /usr/local/go/src/runtime/runtime-gdb.py' > /root/.gdbinit
# 編譯 go 文件. 關閉所有的優化, 防止調試時與編寫的內容不一致
go build -gcflags "all=-N -l" main.go
# 進行調試
gdb ./main

是不是很簡單呀.

調試與揭祕

爲了方便調試, 我將無關內容去掉, 調試使用的程序如下:

package main

func main() {
	var obj interface{}
	type st struct{}
	var s *st
	println(obj == nil)
	obj = s
	println(obj == nil)
}

我們分別在obj賦值前後, 打印局部變量:

image-20220802210607987

image-20220802210623591

我們驚奇的發現, 在obj被賦值之前, obj == nilTRUE, 但是打印變量後發現, obj並不是一個空指針.

而在obj賦值之後, obj == nilFALSE. 前後的差異就在於_type字段.

在此處, 我有理由得出這樣的結論:

  • golang中的interface的實現是一個結構體, 包括_type/data兩個字段
  • 判斷interface是否爲nil時, 若兩個字段均爲nil, 則interfacenil, 否則不爲nil.

同時, 我又好奇的查看了一下obj的類型:

image-20220802210829177

正如上面所看到的, interface是一個特殊的類型, 其在實現上是一個叫做runtime.eface的結構體.

解惑. OK, 到這裏, 就已經解答了我們最開的時候的疑惑, 在將一個空指針對象賦值給internface的時候, 會給interface結構體的字段_type賦值, 使得_type字段不爲nil, 進而導致interface變量不爲nil.

以上, 是我本次問題查找的原因及初步查找的過程. 我基於此對接口的實現原理進行了查閱. 後面就直接進行原理介紹, 不再穿插查找過程了, 否則着實影響觀看體驗.

接口原理

GO在存儲接口類型的變量時, 根據接口中是否包含方法, 分別存儲爲不同類型的結構體.

若接口中不包含方法, 將其存儲爲runtime.eface. 如:

type TestInter interface {
}
var obj interface{}
var obj2 TestInter

若接口中含有方法, 則將其存儲爲runtime.iface. 如:

type TestInter interface {
  testFunc()
}
var obj2 TestInter

eface

eface定義在文件runtime2.go中. 其結構體定義如下:

type eface struct {
	_type *_type // 保存類型信息
	data  unsafe.Pointer // 保存內容
}

type _type struct {
	size       uintptr // 類型大小
	ptrdata    uintptr // 沒整明白是幹什麼用的...
	hash       uint32 // 類型的哈希值. 可用於快速判斷類型是否相等
	tflag      tflag // 類型的額外信息
	align      uint8 // 變量的內存對齊大小
	fieldAlign uint8 
	kind       uint8 // 類型
	equal func(unsafe.Pointer, unsafe.Pointer) bool // 比較此類型對象是否相等
	gcdata    *byte // 垃圾收集的 GC 數據
	str       nameOff
	ptrToThis typeOff
}

// Pointer 就是一個指針
type Pointer *ArbitraryType
type ArbitraryType int

可以看到, 在_type中基本上已經存儲了一個類型的所有信息. (雖然有幾個字段還沒整明白, 不過對於理解整體邏輯影響不大)

_type用來對類型進行標識, 想比底層反射的實現也是根據他來的.

iface

iface區別於eface的地方, 就是iface需要額外存儲接口的方法信息. 若是一個不含有方法的接口, 是可以接收所有值得. 但帶有方法的接口, 則被賦值的內容必須實現了所有的方法. 其結構體定義如下:

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

type itab struct {
	inter *interfacetype // 保存接口的信息. 用於確定變量的接口類型
	_type *_type // data 指向值得類型信息, 上面已經出現過了
	hash  uint32 // 從 _type.hash 拿過來的. 當將 interface 類型變量向下轉型時, 用於快速判斷. 
	_     [4]byte
	// 記錄接口實現的所有方法. 
	// 若 fun[0]==0, 說明 _type 沒有實現此接口. 
	//		(沒錯, 是有可能沒實現的. 比如轉型失敗)
	// 否則, 說明實現了此接口. 所有方法的函數指針在內存中順序存放. 
	// fun[0] 記錄的是第一個方法的地址
	// 順便提一句, 函數按照名稱的字段序在內存中存放
	fun   [1]uintptr 
}

type interfacetype struct {
	typ     _type // 接口類型
	pkgpath name // 包名
	mhdr    []imethod // 接口定義的方法集
}

現在知道我們在將interface類型的變量進行轉型或類型斷言的時候, GO是如何處理的了吧? 其實接口自己是知道自己的類型的.

另外, 在將一個結構體賦值給interface的時候, GO也在其中進行了特定的操作. 可以在runtime.iface.go文件中, 看到一批以conv開頭的方法, 用來將一個變量轉爲數據指針unsafe.Pointer. 在此先按下不表...

總結

以上, 簡單的瞭解了GO接口的內部實現, 發現接口在實現上和普通的結構體變量十分不同, 其內部是通過一個特定的結構體來記錄信息的. 知道了接口的實現, 我們在平常開發時, 碰到接口就應該注意一下, 若interface判斷不爲nil, 存儲的值也可能爲nil.

最後, GO1.18之後增加了泛型的支持, 以前使用interface接收任意參數的場景 也可以使用泛型替代了.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章