問題
小提示, 若想直接查看原理, 可從接口原理開始查看.
有這樣一段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)
}
先盲猜一下結果.
- 第一次
nil
的判斷, 結果爲true
, 沒什麼疑問吧 - 第二次判斷,
s
爲空指針, 結果爲true
- 第三次判斷,
obj
與s
相等, 故也爲空指針, 結果爲true
.
如果你也是這麼認爲, 那麼結果會令你像我一樣十分驚訝:
???第三次判斷, 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
賦值前後, 打印局部變量:
我們驚奇的發現, 在obj
被賦值之前, obj == nil
爲 TRUE
, 但是打印變量後發現, obj
並不是一個空指針.
而在obj
賦值之後, obj == nil
爲 FALSE
. 前後的差異就在於_type
字段.
在此處, 我有理由得出這樣的結論:
golang
中的interface
的實現是一個結構體, 包括_type/data
兩個字段- 判斷
interface
是否爲nil
時, 若兩個字段均爲nil
, 則interface
爲nil
, 否則不爲nil
.
同時, 我又好奇的查看了一下obj
的類型:
正如上面所看到的, 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
接收任意參數的場景 也可以使用泛型替代了.