Go 接口:nil接口为什么不等于nil?
Go 接口:nil接口為什么不等于nil?
目錄本文主要內(nèi)容:深入了解接口類型的運(yùn)行時(shí)表示層。
-
Go 接口:nil接口為什么不等于nil?
- 一、Go 接口的地位
-
二、接口的靜態(tài)特性與動(dòng)態(tài)特性
- 2.1 接口的靜態(tài)特性與動(dòng)態(tài)特性介紹
- 2.2 “動(dòng)靜皆備”的特性的好處
- 三、nil error 值 != nil
-
四、接口類型變量的內(nèi)部表示
- 第一種:nil 接口變量
- 第二種:空接口類型變量
- 第三種:非空接口類型變量
- 第四種:空接口類型變量與非空接口類型變量的等值比較
- 五、輸出接口類型變量?jī)?nèi)部表示的詳細(xì)信息
- 六、接口類型的裝箱(boxing)原理
- 七、小結(jié)
一、Go 接口的地位
Go 語言核心團(tuán)隊(duì)的技術(shù)負(fù)責(zé)人 Russ Cox 也曾說過這樣一句話:“如果要從 Go 語言中挑選出一個(gè)特性放入其他語言,我會(huì)選擇接口”,這句話足以說明接口這一語法特性在這位 Go 語言大神心目中的地位。
為什么接口在 Go 中有這么高的地位呢?這是因?yàn)?strong>接口是 Go 這門靜態(tài)語言中唯一“動(dòng)靜兼?zhèn)洹钡恼Z法特性。而且,接口“動(dòng)靜兼?zhèn)洹钡奶匦越o Go 帶來了強(qiáng)大的表達(dá)能力,但同時(shí)也給 Go 語言初學(xué)者帶來了不少困惑。要想真正解決這些困惑,我們必須深入到 Go 運(yùn)行時(shí)層面,看看 Go 語言在運(yùn)行時(shí)是如何表示接口類型的。
接下來,我們先來看看接口的靜態(tài)與動(dòng)態(tài)特性,看看“動(dòng)靜皆備”的含義。
二、接口的靜態(tài)特性與動(dòng)態(tài)特性
2.1 接口的靜態(tài)特性與動(dòng)態(tài)特性介紹
接口的靜態(tài)特性體現(xiàn)在接口類型變量具有靜態(tài)類型。
比如 var err error 中變量 err 的靜態(tài)類型為 error。擁有靜態(tài)類型,那就意味著編譯器會(huì)在編譯階段對(duì)所有接口類型變量的賦值操作進(jìn)行類型檢查,編譯器會(huì)檢查右值的類型是否實(shí)現(xiàn)了該接口方法集合中的所有方法。如果不滿足,就會(huì)報(bào)錯(cuò):
var err error = 1 // cannot use 1 (type int) as type error in assignment: int does not implement error (missing Error method)
**而接口的動(dòng)態(tài)特性,就體現(xiàn)在接口類型變量在運(yùn)行時(shí)還存儲(chǔ)了右值的真實(shí)類型信息,這個(gè)右值的真實(shí)類型被稱為接口類型變量的動(dòng)態(tài)類型。例如,下面示例代碼:
var err error
err = errors.New("error1")
fmt.Printf("%T\n", err) // *errors.errorString
我們可以看到,這個(gè)示例通過 errros.New 構(gòu)造了一個(gè)錯(cuò)誤值,賦值給了 error 接口類型變量 err,并通過 fmt.Printf 函數(shù)輸出接口類型變量 err 的動(dòng)態(tài)類型為 *errors.errorString。
2.2 “動(dòng)靜皆備”的特性的好處
首先,接口類型變量在程序運(yùn)行時(shí)可以被賦值為不同的動(dòng)態(tài)類型變量,每次賦值后,接口類型變量中存儲(chǔ)的動(dòng)態(tài)類型信息都會(huì)發(fā)生變化,這讓 Go 語言可以像動(dòng)態(tài)語言(比如 Python)那樣擁有使用 Duck Typing(鴨子類型)的靈活性。所謂鴨子類型,就是指某類型所表現(xiàn)出的特性(比如是否可以作為某接口類型的右值),不是由其基因(比如 C++ 中的父類)決定的,而是由類型所表現(xiàn)出來的行為(比如類型擁有的方法)決定的。
比如下面的例子:
type QuackableAnimal interface {
Quack()
}
type Duck struct{}
func (Duck) Quack() {
println("duck quack!")
}
type Dog struct{}
func (Dog) Quack() {
println("dog quack!")
}
type Bird struct{}
func (Bird) Quack() {
println("bird quack!")
}
func AnimalQuackInForest(a QuackableAnimal) {
a.Quack()
}
func main() {
animals := []QuackableAnimal{new(Duck), new(Dog), new(Bird)}
for _, animal := range animals {
AnimalQuackInForest(animal)
}
}
這個(gè)例子中,我們用接口類型 QuackableAnimal 來代表具有“會(huì)叫”這一特征的動(dòng)物,而 Duck、Bird 和 Dog 類型各自都具有這樣的特征,于是我們可以將這三個(gè)類型的變量賦值給 QuackableAnimal 接口類型變量 a。每次賦值,變量 a 中存儲(chǔ)的動(dòng)態(tài)類型信息都不同,Quack 方法的執(zhí)行結(jié)果將根據(jù)變量 a 中存儲(chǔ)的動(dòng)態(tài)類型信息而定。
這里的 Duck、Bird、Dog 都是“鴨子類型”,但它們之間并沒有什么聯(lián)系,之所以能作為右值賦值給 QuackableAnimal 類型變量,只是因?yàn)樗麄儽憩F(xiàn)出了 QuackableAnimal 所要求的特征罷了。
不過,與動(dòng)態(tài)語言不同的是,Go 接口還可以保證“動(dòng)態(tài)特性”使用時(shí)的安全性。比如,編譯器在編譯期就可以捕捉到將 int 類型變量傳給 QuackableAnimal 接口類型變量這樣的明顯錯(cuò)誤,決不會(huì)讓這樣的錯(cuò)誤遺漏到運(yùn)行時(shí)才被發(fā)現(xiàn)。
接口類型的動(dòng)靜特性展示了其強(qiáng)大的一面,然而在日常使用中,對(duì)Gopher常常困惑與“nil 的 error 值不等于 nil”。下面我們來詳細(xì)看一下。
三、nil error 值 != nil
我們先來看一段改編自GO FAQ 中的例子的代碼:
type MyError struct {
error
}
var ErrBad = MyError{
error: errors.New("bad things happened"),
}
func bad() bool {
return false
}
func returnsError() error {
var p *MyError = nil
if bad() {
p = &ErrBad
}
return p
}
func main() {
err := returnsError()
if err != nil {
fmt.Printf("error occur: %+v\n", err)
return
}
fmt.Println("ok")
}
在這個(gè)例子中,我們的關(guān)注點(diǎn)集中在 returnsError 這個(gè)函數(shù)上面。這個(gè)函數(shù)定義了一個(gè) *MyError 類型的變量 p,初值為 nil。如果函數(shù) bad 返回 false,returnsError 函數(shù)就會(huì)直接將 p(此時(shí) p = nil)作為返回值返回給調(diào)用者,之后調(diào)用者會(huì)將 returnsError 函數(shù)的返回值(error 接口類型)與 nil 進(jìn)行比較,并根據(jù)比較結(jié)果做出最終處理。
我們運(yùn)行這段程序后,輸出如下:
error occur: <nil>
按照預(yù)期:程序執(zhí)行應(yīng)該是p 為 nil,returnsError 返回 p,那么 main 函數(shù)中的 err 就等于 nil,于是程序輸出 ok 后退出。但是我們看到,示例程序并未按照預(yù)期,程序顯然是進(jìn)入了錯(cuò)誤處理分支,輸出了 err 的值。那這里就有一個(gè)問題了:明明 returnsError 函數(shù)返回的 p 值為 nil,為什么卻滿足了 if err != nil 的條件進(jìn)入錯(cuò)誤處理分支呢?
為了弄清楚這個(gè)問題,我們來了解接口類型變量的內(nèi)部表示。
四、接口類型變量的內(nèi)部表示
接口類型“動(dòng)靜兼?zhèn)洹钡奶匦砸矝Q定了它的變量的內(nèi)部表示絕不像一個(gè)靜態(tài)類型變量(如 int、float64)那樣簡(jiǎn)單,我們可以在 $GOROOT/src/runtime/runtime2.go 中找到接口類型變量在運(yùn)行時(shí)的表示:
// $GOROOT/src/runtime/runtime2.go
type iface struct {
tab *itab
data unsafe.Pointer
}
type eface struct {
_type *_type
data unsafe.Pointer
}
我們看到,在運(yùn)行時(shí)層面,接口類型變量有兩種內(nèi)部表示:iface 和 eface,這兩種表示分別用于不同的接口類型變量:
-
eface用于表示沒有方法的空接口(empty interface)類型變量,也就是interface{}類型的變量; -
iface用于表示其余擁有方法的接口interface類型變量。
這兩個(gè)結(jié)構(gòu)的共同點(diǎn)是它們都有兩個(gè)指針字段,并且第二個(gè)指針字段的功能相同,都是指向當(dāng)前賦值給該接口類型變量的動(dòng)態(tài)類型變量的值。
那它們的不同點(diǎn)在哪呢?就在于 eface 表示的空接口類型并沒有方法列表,因此它的第一個(gè)指針字段指向一個(gè) _type 類型結(jié)構(gòu),這個(gè)結(jié)構(gòu)為該接口類型變量的動(dòng)態(tài)類型的信息,它的定義是這樣的:
// $GOROOT/src/runtime/type.go
type _type struct {
size uintptr
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32
tflag tflag
align uint8
fieldAlign uint8
kind uint8
// function for comparing objects of this type
// (ptr to object A, ptr to object B) -> ==?
equal func(unsafe.Pointer, unsafe.Pointer) bool
// gcdata stores the GC type data for the garbage collector.
// If the KindGCProg bit is set in kind, gcdata is a GC program.
// Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
gcdata *byte
str nameOff
ptrToThis typeOff
}
而 iface 除了要存儲(chǔ)動(dòng)態(tài)類型信息之外,還要存儲(chǔ)接口本身的信息(接口的類型信息、方法列表信息等)以及動(dòng)態(tài)類型所實(shí)現(xiàn)的方法的信息,因此 iface 的第一個(gè)字段指向一個(gè) itab 類型結(jié)構(gòu)。itab 結(jié)構(gòu)的定義如下:
// $GOROOT/src/runtime/runtime2.go
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.
}
這里我們也可以看到,itab 結(jié)構(gòu)中的第一個(gè)字段 inter 指向的 interfacetype 結(jié)構(gòu),存儲(chǔ)著這個(gè)接口類型自身的信息。你看一下下面這段代碼表示的 interfacetype 類型定義,這個(gè) interfacetype 結(jié)構(gòu)由類型信息(typ)、包路徑名(pkgpath)和接口方法集合切片(mhdr)組成。
// $GOROOT/src/runtime/type.go
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod
}
itab 結(jié)構(gòu)中的字段 _type 則存儲(chǔ)著這個(gè)接口類型變量的動(dòng)態(tài)類型的信息,字段 fun 則是動(dòng)態(tài)類型已實(shí)現(xiàn)的接口方法的調(diào)用地址數(shù)組。
下面我們?cè)俳Y(jié)合例子用圖片來直觀展現(xiàn) eface 和 iface 的結(jié)構(gòu)。首先我們看一個(gè)用 eface 表示的空接口類型變量的例子:
type T struct {
n int
s string
}
func main() {
var t = T {
n: 17,
s: "hello, interface",
}
var ei interface{} = t // Go運(yùn)行時(shí)使用eface結(jié)構(gòu)表示ei
}
這個(gè)例子中的空接口類型變量 ei 在 Go 運(yùn)行時(shí)的表示是這樣的:
我們看到空接口類型的表示較為簡(jiǎn)單,圖中上半部分 _type 字段指向它的動(dòng)態(tài)類型 T 的類型信息,下半部分的 data 則是指向一個(gè) T 類型的實(shí)例值。
我們?cè)賮砜匆粋€(gè)更復(fù)雜的用 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
}
和 eface 比起來,iface 的表示稍微復(fù)雜些。我也畫了一幅表示上面 NonEmptyInterface 接口類型變量在 Go 運(yùn)行時(shí)表示的示意圖:
由上面的這兩幅圖,我們可以看出,每個(gè)接口類型變量在運(yùn)行時(shí)的表示都是由兩部分組成的,針對(duì)不同接口類型我們可以簡(jiǎn)化記作:eface(_type, data) 和 iface(tab, data)。
而且,雖然 eface 和 iface 的第一個(gè)字段有所差別,但 tab 和 _type 可以統(tǒng)一看作是動(dòng)態(tài)類型的類型信息。Go 語言中每種類型都會(huì)有唯一的 _type 信息,無論是內(nèi)置原生類型,還是自定義類型都有。Go 運(yùn)行時(shí)會(huì)為程序內(nèi)的全部類型建立只讀的共享 _type 信息表,因此擁有相同動(dòng)態(tài)類型的同類接口類型變量的 _type/tab 信息是相同的。
而接口類型變量的 data 部分則是指向一個(gè)動(dòng)態(tài)分配的內(nèi)存空間,這個(gè)內(nèi)存空間存儲(chǔ)的是賦值給接口類型變量的動(dòng)態(tài)類型變量的值。未顯式初始化的接口類型變量的值為nil,也就是這個(gè)變量的 _type/tab 和 data 都為 nil。
也就是說,我們判斷兩個(gè)接口類型變量是否相等,只需判斷 _type/tab 以及 data 是否都相等即可。兩個(gè)接口變量的 _type/tab 不同時(shí),即兩個(gè)接口變量的動(dòng)態(tài)類型不相同時(shí),兩個(gè)接口類型變量一定不等。
當(dāng)兩個(gè)接口變量的 _type/tab 相同時(shí),對(duì) data 的相等判斷要有區(qū)分。當(dāng)接口變量的動(dòng)態(tài)類型為指針類型時(shí) (*T),Go 不會(huì)再額外分配內(nèi)存存儲(chǔ)指針值,而會(huì)將動(dòng)態(tài)類型的指針值直接存入 data 字段中,這樣 data 值的相等性決定了兩個(gè)接口類型變量是否相等;當(dāng)接口變量的動(dòng)態(tài)類型為非指針類型 (T) 時(shí),我們判斷的將不是 data 指針的值是否相等,而是判斷 data 指針指向的內(nèi)存空間所存儲(chǔ)的數(shù)據(jù)值是否相等,若相等,則兩個(gè)接口類型變量相等。
不過,通過肉眼去辨別接口類型變量是否相等總是困難一些,我們可以引入一些 helper 函數(shù)。借助這些函數(shù),我們可以清晰地輸出接口類型變量的內(nèi)部表示,這樣就可以一目了然地看出兩個(gè)變量是否相等了。
由于 eface 和 iface 是 runtime 包中的非導(dǎo)出結(jié)構(gòu)體定義,我們不能直接在包外使用,所以也就無法直接訪問到兩個(gè)結(jié)構(gòu)體中的數(shù)據(jù)。不過,Go 語言提供了 println 預(yù)定義函數(shù),可以用來輸出 eface 或 iface 的兩個(gè)指針字段的值。
在編譯階段,編譯器會(huì)根據(jù)要輸出的參數(shù)的類型將 println 替換為特定的函數(shù),這些函數(shù)都定義在 $GOROOT/src/runtime/print.go 文件中,而針對(duì) eface 和 iface 類型的打印函數(shù)實(shí)現(xiàn)如下:
// $GOROOT/src/runtime/print.go
func printeface(e eface) {
print("(", e._type, ",", e.data, ")")
}
func printiface(i iface) {
print("(", i.tab, ",", i.data, ")")
}
我們看到,printeface 和 printiface 會(huì)輸出各自的兩個(gè)指針字段的值。下面我們就來使用 println 函數(shù)輸出各類接口類型變量的內(nèi)部表示信息,并結(jié)合輸出結(jié)果,解析接口類型變量的等值比較操作。
第一種:nil 接口變量
我們知道,未賦初值的接口類型變量的值為 nil,這類變量也就是 nil 接口變量,我們來看這類變量的內(nèi)部表示輸出的例子:
func printNilInterface() {
// nil接口變量
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)
}
運(yùn)行這個(gè)函數(shù),輸出結(jié)果是這樣的:
(0x0,0x0)
(0x0,0x0)
i = nil: true
err = nil: true
i = err: true
我們看到,無論是空接口類型還是非空接口類型變量,一旦變量值為 nil,那么它們內(nèi)部表示均為 (0x0, 0x0),也就是類型信息、數(shù)據(jù)值信息均為空。因此上面的變量 i 和 err 等值判斷為 true。
第二種:空接口類型變量
下面是空接口類型變量的內(nèi)部表示輸出的例子:
func printEmptyInterface() {
var eif1 interface{} // 空接口類型
var eif2 interface{} // 空接口類型
var n, m int = 17, 18
eif1 = n
eif2 = m
println("eif1:", eif1)
println("eif2:", eif2)
println("eif1 = eif2:", eif1 == eif2) // false
eif2 = 17
println("eif1:", eif1)
println("eif2:", eif2)
println("eif1 = eif2:", eif1 == eif2) // true
eif2 = int64(17)
println("eif1:", eif1)
println("eif2:", eif2)
println("eif1 = eif2:", eif1 == eif2) // false
}
這個(gè)例子的運(yùn)行輸出結(jié)果是這樣的:
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
我們按順序分析一下這個(gè)輸出結(jié)果。
首先,代碼執(zhí)行到第 11 行時(shí),eif1 與 eif2 已經(jīng)分別被賦值整型值 17 與 18,這樣 eif1 和 eif2 的動(dòng)態(tài)類型的類型信息是相同的(都是 0x10ac580),但 data 指針指向的內(nèi)存塊中存儲(chǔ)的值不同,一個(gè)是 17,一個(gè)是 18,于是 eif1 不等于 eif2。
接著,代碼執(zhí)行到第 16 行的時(shí)候,eif2 已經(jīng)被重新賦值為 17,這樣 eif1 和 eif2 不僅存儲(chǔ)的動(dòng)態(tài)類型的類型信息是相同的(都是 0x10ac580),data 指針指向的內(nèi)存塊中存儲(chǔ)值也相同了,都是 17,于是 eif1 等于 eif2。
然后,代碼執(zhí)行到第 21 行時(shí),eif2 已經(jīng)被重新賦值了 int64 類型的數(shù)值 17。這樣,eif1 和 eif2 存儲(chǔ)的動(dòng)態(tài)類型的類型信息就變成不同的了,一個(gè)是 int,一個(gè)是 int64,即便 data 指針指向的內(nèi)存塊中存儲(chǔ)值是相同的,最終 eif1 與 eif2 也是不相等的。
第三種:非空接口類型變量
這里,我們也直接來看一個(gè)非空接口類型變量的內(nèi)部表示輸出的例子:
type T int
func (t T) Error() string {
return "bad error"
}
func printNonEmptyInterface() {
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)
}
這個(gè)例子的運(yùn)行輸出結(jié)果如下:
err1: (0x10ed120,0x0)
err1 = nil: false
err1: (0x10ed1a0,0x10eb310)
err2: (0x10ed1a0,0x10eb318)
err1 = err2: false
err1: (0x10ed1a0,0x10eb310)
err2: (0x10ed0c0,0xc000010050)
err1 = err2: false
我們看到上面示例中每一輪通過 println 輸出的 err1 和 err2 的 tab 和 data 值,要么 data 值不同,要么 tab 與 data 值都不同。
和空接口類型變量一樣,只有 tab 和 data 指的數(shù)據(jù)內(nèi)容一致的情況下,兩個(gè)非空接口類型變量之間才能劃等號(hào)。這里我們要注意 err1 下面的賦值情況:
err1 = (*T)(nil)
針對(duì)這種賦值,println 輸出的 err1 是(0x10ed120, 0x0),也就是非空接口類型變量的類型信息并不為空,數(shù)據(jù)指針為空,因此它與 nil(0x0, 0x0)之間不能劃等號(hào)。
現(xiàn)在我們?cè)倩氐轿覀冮_頭的那個(gè)問題,你是不是已經(jīng)豁然開朗了呢?開頭的問題中,從 returnsError 返回的 error 接口類型變量 err 的數(shù)據(jù)指針雖然為空,但它的類型信息(iface.tab)并不為空,而是 *MyError 對(duì)應(yīng)的類型信息,這樣 err 與 nil(0x0,0x0)相比自然不相等,這就是我們開頭那個(gè)問題的答案解析,現(xiàn)在你明白了嗎?
第四種:空接口類型變量與非空接口類型變量的等值比較
下面是非空接口類型變量和空接口類型變量之間進(jìn)行比較的例子:
func printEmptyInterfaceAndNonEmptyInterface() {
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)
}
這個(gè)示例的輸出結(jié)果如下:
eif: (0x10b3b00,0x10eb4d0)
err: (0x10ed380,0x10eb4d8)
eif = err: true
eif: (0x10b3b00,0x10eb4d0)
err: (0x10ed380,0x10eb4e0)
eif = err: false
你可以看到,空接口類型變量和非空接口類型變量?jī)?nèi)部表示的結(jié)構(gòu)有所不同(第一個(gè)字段:_type vs. tab),兩者似乎一定不能相等。但 Go 在進(jìn)行等值比較時(shí),類型比較使用的是 eface 的 _type 和 iface 的 tab._type,因此就像我們?cè)谶@個(gè)例子中看到的那樣,當(dāng) eif 和 err 都被賦值為 T(5) 時(shí),兩者之間是劃等號(hào)的。
好了,到這里,我們已經(jīng)理解了各類接口類型變量在運(yùn)行時(shí)層的表示。我們可以通過 println 可以查看這個(gè)表示信息,從中我們也知道了接口變量只有在類型信息與值信息都一致的情況下才能劃等號(hào)。
五、輸出接口類型變量?jī)?nèi)部表示的詳細(xì)信息
不過,println 輸出的接口類型變量的內(nèi)部表示信息,在一般情況下都是足夠的,但有些時(shí)候又顯得過于簡(jiǎn)略,比如在上面最后一個(gè)例子中,如果僅憑 eif: (0x10b3b00,0x10eb4d0) 和 err: (0x10ed380,0x10eb4d8) 的輸出,我們是無法想到兩個(gè)變量是相等的。
那這時(shí)如果我們能輸出接口類型變量?jī)?nèi)部表示的詳細(xì)信息(比如:tab._type),那勢(shì)必可以取得事半功倍的效果。接下來我們就看看這要怎么做。
前面提到過,eface 和 iface 以及組成它們的 itab 和 _type 都是 runtime 包下的非導(dǎo)出結(jié)構(gòu)體,我們無法在外部直接引用它們。但我們發(fā)現(xiàn),組成 eface、iface 的類型都是基本數(shù)據(jù)類型,我們完全可以通過“復(fù)制代碼”的方式將它們拿到 runtime 包外面來。
不過,這里要注意,由于 runtime 中的 eface、iface,或者它們的組成可能會(huì)隨著 Go 版本的變化發(fā)生變化,因此這個(gè)方法不具備跨版本兼容性。也就是說,基于 Go 1.17 版本復(fù)制的代碼,可能僅適用于使用 Go 1.17 版本編譯。這里我們就以 Go 1.17 版本為例看看:
// dumpinterface.go
type eface struct {
_type *_type
data unsafe.Pointer
}
type tflag uint8
type nameOff int32
type typeOff int32
type _type struct {
size uintptr
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32
tflag tflag
align uint8
fieldAlign uint8
kind uint8
// function for comparing objects of this type
// (ptr to object A, ptr to object B) -> ==?
equal func(unsafe.Pointer, unsafe.Pointer) bool
// gcdata stores the GC type data for the garbage collector.
// If the KindGCProg bit is set in kind, gcdata is a GC program.
// Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
gcdata *byte
str nameOff
ptrToThis typeOff
}
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.
}
... ...
const ptrSize = unsafe.Sizeof(uintptr(0))
func dumpEface(i interface{}) {
ptrToEface := (*eface)(unsafe.Pointer(&i))
fmt.Printf("eface: %+v\n", *ptrToEface)
if ptrToEface._type != nil {
// dump _type info
fmt.Printf("\t _type: %+v\n", *(ptrToEface._type))
}
if ptrToEface.data != nil {
// dump data
switch i.(type) {
case int:
dumpInt(ptrToEface.data)
case float64:
dumpFloat64(ptrToEface.data)
case T:
dumpT(ptrToEface.data)
// other cases ... ...
default:
fmt.Printf("\t unsupported data type\n")
}
}
fmt.Printf("\n")
}
func dumpItabOfIface(ptrToIface unsafe.Pointer) {
p := (*iface)(ptrToIface)
fmt.Printf("iface: %+v\n", *p)
if p.tab != nil {
// dump itab
fmt.Printf("\t itab: %+v\n", *(p.tab))
// dump inter in itab
fmt.Printf("\t\t inter: %+v\n", *(p.tab.inter))
// dump _type in itab
fmt.Printf("\t\t _type: %+v\n", *(p.tab._type))
// dump fun in tab
funPtr := unsafe.Pointer(&(p.tab.fun))
fmt.Printf("\t\t fun: [")
for i := 0; i < len((*(p.tab.inter)).mhdr); i++ {
tp := (*uintptr)(unsafe.Pointer(uintptr(funPtr) + uintptr(i)*ptrSize))
fmt.Printf("0x%x(%d),", *tp, *tp)
}
fmt.Printf("]\n")
}
}
func dumpDataOfIface(i interface{}) {
// this is a trick as the data part of eface and iface are same
ptrToEface := (*eface)(unsafe.Pointer(&i))
if ptrToEface.data != nil {
// dump data
switch i.(type) {
case int:
dumpInt(ptrToEface.data)
case float64:
dumpFloat64(ptrToEface.data)
case T:
dumpT(ptrToEface.data)
// other cases ... ...
default:
fmt.Printf("\t unsupported data type\n")
}
}
fmt.Printf("\n")
}
func dumpT(dataOfIface unsafe.Pointer) {
var p *T = (*T)(dataOfIface)
fmt.Printf("\t data: %+v\n", *p)
}
... ...
這里只挑選了關(guān)鍵部分,省略了部分代碼。上面這個(gè) dumpinterface.go 中提供了三個(gè)主要函數(shù):
-
dumpEface: 用于輸出空接口類型變量的內(nèi)部表示信息; -
dumpItabOfIface: 用于輸出非空接口類型變量的tab字段信息; -
dumpDataOfIface: 用于輸出非空接口類型變量的data字段信息;
我們利用這三個(gè)函數(shù)來輸出一下前面 printEmptyInterfaceAndNonEmptyInterface 函數(shù)中的接口類型變量的信息:
package main
import "unsafe"
type T int
func (t T) Error() string {
return "bad error"
}
func main() {
var eif interface{} = T(5)
var err error = T(5)
println("eif:", eif)
println("err:", err)
println("eif = err:", eif == err)
dumpEface(eif)
dumpItabOfIface(unsafe.Pointer(&err))
dumpDataOfIface(err)
}
運(yùn)行這個(gè)示例代碼,我們得到了這個(gè)輸出結(jié)果:
eif: (0x10b38c0,0x10e9b30)
err: (0x10eb690,0x10e9b30)
eif = err: true
eface: {_type:0x10b38c0 data:0x10e9b30}
_type: {size:8 ptrdata:0 hash:1156555957 tflag:15 align:8 fieldAlign:8 kind:2 equal:0x10032e0 gcdata:0x10e9a60 str:4946 ptrToThis:58496}
data: bad error
iface: {tab:0x10eb690 data:0x10e9b30}
itab: {inter:0x10b5e20 _type:0x10b38c0 hash:1156555957 _:[0 0 0 0] fun:[17454976]}
inter: {typ:{size:16 ptrdata:16 hash:235953867 tflag:7 align:8 fieldAlign:8 kind:20 equal:0x10034c0 gcdata:0x10d2418 str:3666 ptrToThis:26848} pkgpath:{bytes:<nil>} mhdr:[{name:2592 ityp:43520}]}
_type: {size:8 ptrdata:0 hash:1156555957 tflag:15 align:8 fieldAlign:8 kind:2 equal:0x10032e0 gcdata:0x10e9a60 str:4946 ptrToThis:58496}
fun: [0x10a5780(17454976),]
data: bad error
從輸出結(jié)果中,我們看到 eif 的 _type(0x10b38c0)與 err 的 tab._type(0x10b38c0)是一致的,data 指針?biāo)竷?nèi)容(“bad error”)也是一致的,因此 eif == err 表達(dá)式的結(jié)果為 true。
再次強(qiáng)調(diào)一遍,上面這個(gè)實(shí)現(xiàn)可能僅在 Go 1.17 版本上測(cè)試通過,并且在輸出 iface 或 eface 的 data 部分內(nèi)容時(shí)只列出了 int、float64 和 T 類型的數(shù)據(jù)讀取實(shí)現(xiàn),沒有列出全部類型的實(shí)現(xiàn),你可以根據(jù)自己的需要實(shí)現(xiàn)其余數(shù)據(jù)類型。dumpinterface.go 的完整代碼你可以在這里找到。
我們現(xiàn)在已經(jīng)知道了,接口類型有著復(fù)雜的內(nèi)部結(jié)構(gòu),所以我們將一個(gè)類型變量值賦值給一個(gè)接口類型變量值的過程肯定不會(huì)像 var i int = 5 那么簡(jiǎn)單,那么接口類型變量賦值的過程是怎樣的呢?其實(shí)接口類型變量賦值是一個(gè)“裝箱”的過程。
六、接口類型的裝箱(boxing)原理
裝箱(boxing)是編程語言領(lǐng)域的一個(gè)基礎(chǔ)概念,一般是指把一個(gè)值類型轉(zhuǎn)換成引用類型,比如在支持裝箱概念的 Java 語言中,將一個(gè) int 變量轉(zhuǎn)換成 Integer 對(duì)象就是一個(gè)裝箱操作。
在 Go 語言中,將任意類型賦值給一個(gè)接口類型變量也是裝箱操作。有了前面對(duì)接口類型變量?jī)?nèi)部表示的學(xué)習(xí),我們知道接口類型的裝箱實(shí)際就是創(chuàng)建一個(gè) eface 或 iface 的過程。接下來我們就來簡(jiǎn)要描述一下這個(gè)過程,也就是接口類型的裝箱原理。
我們基于下面這個(gè)例子中的接口裝箱操作來說明:
// interface_internal.go
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)
}
這個(gè)例子中,對(duì) ei 和 i 兩個(gè)接口類型變量的賦值都會(huì)觸發(fā)裝箱操作,要想知道 Go 在背后做了些什么,我們需要“下沉”一層,也就是要輸出上面 Go 代碼對(duì)應(yīng)的匯編代碼:
$go tool compile -S interface_internal.go > interface_internal.s
對(duì)應(yīng) ei = t 一行的匯編如下:
0x0026 00038 (interface_internal.go:24) MOVQ $17, ""..autotmp_15+104(SP)
0x002f 00047 (interface_internal.go:24) LEAQ go.string."hello, interface"(SB), CX
0x0036 00054 (interface_internal.go:24) MOVQ CX, ""..autotmp_15+112(SP)
0x003b 00059 (interface_internal.go:24) MOVQ $16, ""..autotmp_15+120(SP)
0x0044 00068 (interface_internal.go:24) LEAQ type."".T(SB), AX
0x004b 00075 (interface_internal.go:24) LEAQ ""..autotmp_15+104(SP), BX
0x0050 00080 (interface_internal.go:24) PCDATA $1, $0
0x0050 00080 (interface_internal.go:24) CALL runtime.convT2E(SB)
對(duì)應(yīng) i = t 一行的匯編如下:
0x005f 00095 (interface_internal.go:27) MOVQ $17, ""..autotmp_15+104(SP)
0x0068 00104 (interface_internal.go:27) LEAQ go.string."hello, interface"(SB), CX
0x006f 00111 (interface_internal.go:27) MOVQ CX, ""..autotmp_15+112(SP)
0x0074 00116 (interface_internal.go:27) MOVQ $16, ""..autotmp_15+120(SP)
0x007d 00125 (interface_internal.go:27) LEAQ go.itab."".T,"".NonEmptyInterface(SB), AX
0x0084 00132 (interface_internal.go:27) LEAQ ""..autotmp_15+104(SP), BX
0x0089 00137 (interface_internal.go:27) PCDATA $1, $1
0x0089 00137 (interface_internal.go:27) CALL runtime.convT2I(SB)
在將動(dòng)態(tài)類型變量賦值給接口類型變量語句對(duì)應(yīng)的匯編代碼中,我們看到了 convT2E 和 convT2I 兩個(gè) runtime 包的函數(shù)。這兩個(gè)函數(shù)的實(shí)現(xiàn)位于 $GOROOT/src/runtime/iface.go 中:
// $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 用于將任意類型轉(zhuǎn)換為一個(gè) eface,convT2I 用于將任意類型轉(zhuǎn)換為一個(gè) iface。兩個(gè)函數(shù)的實(shí)現(xiàn)邏輯相似,主要思路就是根據(jù)傳入的類型信息(convT2E 的 _type 和 convT2I 的 tab._type)分配一塊內(nèi)存空間,并將 elem 指向的數(shù)據(jù)拷貝到這塊內(nèi)存空間中,最后傳入的類型信息作為返回值結(jié)構(gòu)中的類型信息,返回值結(jié)構(gòu)中的數(shù)據(jù)指針(data)指向新分配的那塊內(nèi)存空間。
由此我們也可以看出,經(jīng)過裝箱后,箱內(nèi)的數(shù)據(jù),也就是存放在新分配的內(nèi)存空間中的數(shù)據(jù)與原變量便無瓜葛了,比如下面這個(gè)例子:
func main() {
var n int = 61
var ei interface{} = n
n = 62 // n的值已經(jīng)改變
fmt.Println("data in box:", ei) // 輸出仍是61
}
那么 convT2E 和 convT2I 函數(shù)的類型信息是從何而來的呢?
其實(shí)這些都依賴 Go 編譯器的工作。編譯器知道每個(gè)要轉(zhuǎn)換為接口類型變量(toType)和動(dòng)態(tài)類型變量的類型(fromType),它會(huì)根據(jù)這一對(duì)類型選擇適當(dāng)?shù)?convT2X 函數(shù),并在生成代碼時(shí)使用選出的 convT2X 函數(shù)參與裝箱操作。
不過,裝箱是一個(gè)有性能損耗的操作,因此 Go 也在不斷對(duì)裝箱操作進(jìn)行優(yōu)化,包括對(duì)常見類型如整型、字符串、切片等提供系列快速轉(zhuǎn)換函數(shù):
// $GOROOT/src/runtime/iface.go
func convT16(val any) unsafe.Pointer // val must be uint16-like
func convT32(val any) unsafe.Pointer // val must be uint32-like
func convT64(val any) unsafe.Pointer // val must be uint64-like
func convTstring(val any) unsafe.Pointer // val must be a string
func convTslice(val any) unsafe.Pointer // val must be a slice
這些函數(shù)去除了 typedmemmove 操作,增加了零值快速返回等特性。
同時(shí) Go 建立了 staticuint64s 區(qū)域,對(duì) 255 以內(nèi)的小整數(shù)值進(jìn)行裝箱操作時(shí)不再分配新內(nèi)存,而是利用 staticuint64s 區(qū)域的內(nèi)存空間,下面是 staticuint64s 的定義:
// $GOROOT/src/runtime/iface.go
// staticuint64s is used to avoid allocating in convTx for small integer values.
var staticuint64s = [...]uint64{
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
... ...
}
七、小結(jié)
接口類型作為參與構(gòu)建 Go 應(yīng)用骨架的重要參與者,在 Go 語言中有著很高的地位。它這個(gè)地位的取得離不開它擁有的“動(dòng)靜兼?zhèn)洹钡恼Z法特性。Go 接口的動(dòng)態(tài)特性讓 Go 擁有與動(dòng)態(tài)語言相近的靈活性,而靜態(tài)特性又在編譯階段保證了這種靈活性的安全。
要更好地理解 Go 接口的這兩種特性,我們需要深入到 Go 接口在運(yùn)行時(shí)的表示層面上去。接口類型變量在運(yùn)行時(shí)表示為 eface 和 iface,eface 用于表示空接口類型變量,iface 用于表示非空接口類型變量。只有兩個(gè)接口類型變量的類型信息(eface._type/iface.tab._type)相同,且數(shù)據(jù)指針(eface.data/iface.data)所指數(shù)據(jù)相同時(shí),兩個(gè)接口類型變量才是相等的。
我們可以通過 println 輸出接口類型變量的兩部分指針變量的值。而且,通過拷貝 runtime 包 eface 和 iface 相關(guān)類型源碼,我們還可以自定義輸出 eface/iface 詳盡信息的函數(shù),不過要注意的是,由于 runtime 層代碼的演進(jìn),這個(gè)函數(shù)可能不具備在 Go 版本間的移植性。
最后,接口類型變量的賦值本質(zhì)上是一種裝箱操作,裝箱操作是由 Go 編譯器和運(yùn)行時(shí)共同完成的,有一定的性能開銷,對(duì)于性能敏感的系統(tǒng)來說,我們應(yīng)該盡量避免或減少這類裝箱操作。
總結(jié)
以上是生活随笔為你收集整理的Go 接口:nil接口为什么不等于nil?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【原型链污染】Python与Js
- 下一篇: Redis系列之常见数据类型应用场景