Go 接口:nil接口為什么不等于nil?
本文主要內(nèi)容:深入了解接口類型的運行時表示層。
-
Go 接口:nil接口為什么不等于nil?
- 一、Go 接口的地位
-
二、接口的靜態(tài)特性與動態(tài)特性
- 2.1 接口的靜態(tài)特性與動態(tài)特性介紹
- 2.2 “動靜皆備”的特性的好處
- 三、nil error 值 != nil
-
四、接口類型變量的內(nèi)部表示
- 第一種:nil 接口變量
- 第二種:空接口類型變量
- 第三種:非空接口類型變量
- 第四種:空接口類型變量與非空接口類型變量的等值比較
- 五、輸出接口類型變量內(nèi)部表示的詳細信息
- 六、接口類型的裝箱(boxing)原理
- 七、小結(jié)
一、Go 接口的地位
Go 語言核心團隊的技術(shù)負責(zé)人 Russ Cox
也曾說過這樣一句話:“如果要從 Go 語言中挑選出一個特性放入其他語言,我會選擇接口”,這句話足以說明接口這一語法特性在這位 Go 語言大神心目中的地位。
為什么接口在 Go 中有這么高的地位呢?這是因為接口是 Go 這門靜態(tài)語言中唯一“動靜兼?zhèn)洹钡恼Z法特性。而且,接口“動靜兼?zhèn)洹钡奶匦越o Go 帶來了強大的表達能力,但同時也給 Go 語言初學(xué)者帶來了不少困惑。要想真正解決這些困惑,我們必須深入到 Go 運行時層面,看看 Go 語言在運行時是如何表示接口類型的。
接下來,我們先來看看接口的靜態(tài)與動態(tài)特性,看看“動靜皆備”的含義。
二、接口的靜態(tài)特性與動態(tài)特性
2.1 接口的靜態(tài)特性與動態(tài)特性介紹
接口的靜態(tài)特性體現(xiàn)在接口類型變量具有靜態(tài)類型。
比如 var err error
中變量 err
的靜態(tài)類型為 error
。擁有靜態(tài)類型,那就意味著編譯器會在編譯階段對所有接口類型變量的賦值操作進行類型檢查,編譯器會檢查右值的類型是否實現(xiàn)了該接口方法集合中的所有方法。如果不滿足,就會報錯:
var err error = 1 // cannot use 1 (type int) as type error in assignment: int does not implement error (missing Error method)
**而接口的動態(tài)特性,就體現(xiàn)在接口類型變量在運行時還存儲了右值的真實類型信息,這個右值的真實類型被稱為接口類型變量的動態(tài)類型。例如,下面示例代碼:
var err error
err = errors.New("error1")
fmt.Printf("%T\n", err) // *errors.errorString
我們可以看到,這個示例通過 errros.New
構(gòu)造了一個錯誤值,賦值給了 error
接口類型變量 err
,并通過 fmt.Printf
函數(shù)輸出接口類型變量 err
的動態(tài)類型為 *errors.errorString
。
2.2 “動靜皆備”的特性的好處
首先,接口類型變量在程序運行時可以被賦值為不同的動態(tài)類型變量,每次賦值后,接口類型變量中存儲的動態(tài)類型信息都會發(fā)生變化,這讓 Go 語言可以像動態(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)
}
}
這個例子中,我們用接口類型 QuackableAnimal
來代表具有“會叫”這一特征的動物,而 Duck
、Bird
和 Dog
類型各自都具有這樣的特征,于是我們可以將這三個類型的變量賦值給 QuackableAnimal
接口類型變量 a
。每次賦值,變量 a
中存儲的動態(tài)類型信息都不同,Quack
方法的執(zhí)行結(jié)果將根據(jù)變量 a
中存儲的動態(tài)類型信息而定。
這里的 Duck
、Bird
、Dog
都是“鴨子類型”,但它們之間并沒有什么聯(lián)系,之所以能作為右值賦值給 QuackableAnimal
類型變量,只是因為他們表現(xiàn)出了 QuackableAnimal
所要求的特征罷了。
不過,與動態(tài)語言不同的是,Go 接口還可以保證“動態(tài)特性”使用時的安全性。比如,編譯器在編譯期就可以捕捉到將 int
類型變量傳給 QuackableAnimal
接口類型變量這樣的明顯錯誤,決不會讓這樣的錯誤遺漏到運行時才被發(fā)現(xiàn)。
接口類型的動靜特性展示了其強大的一面,然而在日常使用中,對Gopher
常常困惑與“nil 的 error 值不等于 nil”。下面我們來詳細看一下。
三、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")
}
在這個例子中,我們的關(guān)注點集中在 returnsError
這個函數(shù)上面。這個函數(shù)定義了一個 *MyError
類型的變量 p
,初值為 nil
。如果函數(shù) bad
返回 false
,returnsError
函數(shù)就會直接將 p
(此時 p = nil
)作為返回值返回給調(diào)用者,之后調(diào)用者會將 returnsError
函數(shù)的返回值(error
接口類型)與 nil
進行比較,并根據(jù)比較結(jié)果做出最終處理。
我們運行這段程序后,輸出如下:
error occur: <nil>
按照預(yù)期:程序執(zhí)行應(yīng)該是p
為 nil
,returnsError
返回 p
,那么 main
函數(shù)中的 err
就等于 nil
,于是程序輸出 ok
后退出。但是我們看到,示例程序并未按照預(yù)期,程序顯然是進入了錯誤處理分支,輸出了 err
的值。那這里就有一個問題了:明明 returnsError
函數(shù)返回的 p
值為 nil
,為什么卻滿足了 if err != nil
的條件進入錯誤處理分支呢?
為了弄清楚這個問題,我們來了解接口類型變量的內(nèi)部表示。
四、接口類型變量的內(nèi)部表示
接口類型“動靜兼?zhèn)洹钡奶匦砸矝Q定了它的變量的內(nèi)部表示絕不像一個靜態(tài)類型變量(如 int
、float64
)那樣簡單,我們可以在 $GOROOT/src/runtime/runtime2.go
中找到接口類型變量在運行時的表示:
// $GOROOT/src/runtime/runtime2.go
type iface struct {
tab *itab
data unsafe.Pointer
}
type eface struct {
_type *_type
data unsafe.Pointer
}
我們看到,在運行時層面,接口類型變量有兩種內(nèi)部表示:iface
和 eface
,這兩種表示分別用于不同的接口類型變量:
-
eface
用于表示沒有方法的空接口(empty interface)類型變量,也就是interface{}
類型的變量; -
iface
用于表示其余擁有方法的接口interface
類型變量。
這兩個結(jié)構(gòu)的共同點是它們都有兩個指針字段,并且第二個指針字段的功能相同,都是指向當(dāng)前賦值給該接口類型變量的動態(tài)類型變量的值。
那它們的不同點在哪呢?就在于 eface
表示的空接口類型并沒有方法列表,因此它的第一個指針字段指向一個 _type
類型結(jié)構(gòu),這個結(jié)構(gòu)為該接口類型變量的動態(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
除了要存儲動態(tài)類型信息之外,還要存儲接口本身的信息(接口的類型信息、方法列表信息等)以及動態(tài)類型所實現(xiàn)的方法的信息,因此 iface
的第一個字段指向一個 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)中的第一個字段 inter
指向的 interfacetype
結(jié)構(gòu),存儲著這個接口類型自身的信息。你看一下下面這段代碼表示的 interfacetype
類型定義,這個 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
則存儲著這個接口類型變量的動態(tài)類型的信息,字段 fun
則是動態(tài)類型已實現(xiàn)的接口方法的調(diào)用地址數(shù)組。
下面我們再結(jié)合例子用圖片來直觀展現(xiàn) eface
和 iface
的結(jié)構(gòu)。首先我們看一個用 eface
表示的空接口類型變量的例子:
type T struct {
n int
s string
}
func main() {
var t = T {
n: 17,
s: "hello, interface",
}
var ei interface{} = t // Go運行時使用eface結(jié)構(gòu)表示ei
}
這個例子中的空接口類型變量 ei
在 Go 運行時的表示是這樣的:
我們看到空接口類型的表示較為簡單,圖中上半部分 _type
字段指向它的動態(tài)類型 T
的類型信息,下半部分的 data
則是指向一個 T
類型的實例值。
我們再來看一個更復(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 運行時表示的示意圖:
由上面的這兩幅圖,我們可以看出,每個接口類型變量在運行時的表示都是由兩部分組成的,針對不同接口類型我們可以簡化記作:eface(_type, data)
和 iface(tab, data)
。
而且,雖然 eface
和 iface
的第一個字段有所差別,但 tab
和 _type
可以統(tǒng)一看作是動態(tài)類型的類型信息。Go 語言中每種類型都會有唯一的 _type
信息,無論是內(nèi)置原生類型,還是自定義類型都有。Go 運行時會為程序內(nèi)的全部類型建立只讀的共享 _type
信息表,因此擁有相同動態(tài)類型的同類接口類型變量的 _type/tab
信息是相同的。
而接口類型變量的 data 部分則是指向一個動態(tài)分配的內(nèi)存空間,這個內(nèi)存空間存儲的是賦值給接口類型變量的動態(tài)類型變量的值。未顯式初始化的接口類型變量的值為nil,也就是這個變量的 _type/tab 和 data 都為 nil。
也就是說,我們判斷兩個接口類型變量是否相等,只需判斷 _type/tab
以及 data
是否都相等即可。兩個接口變量的 _type/tab
不同時,即兩個接口變量的動態(tài)類型不相同時,兩個接口類型變量一定不等。
當(dāng)兩個接口變量的 _type/tab
相同時,對 data
的相等判斷要有區(qū)分。當(dāng)接口變量的動態(tài)類型為指針類型時 (*T
),Go 不會再額外分配內(nèi)存存儲指針值,而會將動態(tài)類型的指針值直接存入 data
字段中,這樣 data
值的相等性決定了兩個接口類型變量是否相等;當(dāng)接口變量的動態(tài)類型為非指針類型 (T
) 時,我們判斷的將不是 data
指針的值是否相等,而是判斷 data
指針指向的內(nèi)存空間所存儲的數(shù)據(jù)值是否相等,若相等,則兩個接口類型變量相等。
不過,通過肉眼去辨別接口類型變量是否相等總是困難一些,我們可以引入一些 helper 函數(shù)。借助這些函數(shù),我們可以清晰地輸出接口類型變量的內(nèi)部表示,這樣就可以一目了然地看出兩個變量是否相等了。
由于 eface
和 iface
是 runtime
包中的非導(dǎo)出結(jié)構(gòu)體定義,我們不能直接在包外使用,所以也就無法直接訪問到兩個結(jié)構(gòu)體中的數(shù)據(jù)。不過,Go 語言提供了 println
預(yù)定義函數(shù),可以用來輸出 eface
或 iface
的兩個指針字段的值。
在編譯階段,編譯器會根據(jù)要輸出的參數(shù)的類型將 println
替換為特定的函數(shù),這些函數(shù)都定義在 $GOROOT/src/runtime/print.go
文件中,而針對 eface
和 iface
類型的打印函數(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
會輸出各自的兩個指針字段的值。下面我們就來使用 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)
}
運行這個函數(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
}
這個例子的運行輸出結(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
我們按順序分析一下這個輸出結(jié)果。
首先,代碼執(zhí)行到第 11 行時,eif1
與 eif2
已經(jīng)分別被賦值整型值 17
與 18
,這樣 eif1
和 eif2
的動態(tài)類型的類型信息是相同的(都是 0x10ac580
),但 data
指針指向的內(nèi)存塊中存儲的值不同,一個是 17
,一個是 18
,于是 eif1
不等于 eif2
。
接著,代碼執(zhí)行到第 16 行的時候,eif2
已經(jīng)被重新賦值為 17
,這樣 eif1
和 eif2
不僅存儲的動態(tài)類型的類型信息是相同的(都是 0x10ac580
),data
指針指向的內(nèi)存塊中存儲值也相同了,都是 17
,于是 eif1
等于 eif2
。
然后,代碼執(zhí)行到第 21 行時,eif2
已經(jīng)被重新賦值了 int64
類型的數(shù)值 17
。這樣,eif1
和 eif2
存儲的動態(tài)類型的類型信息就變成不同的了,一個是 int
,一個是 int64
,即便 data
指針指向的內(nèi)存塊中存儲值是相同的,最終 eif1
與 eif2
也是不相等的。
第三種:非空接口類型變量
這里,我們也直接來看一個非空接口類型變量的內(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)
}
這個例子的運行輸出結(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)容一致的情況下,兩個非空接口類型變量之間才能劃等號。這里我們要注意 err1
下面的賦值情況:
err1 = (*T)(nil)
針對這種賦值,println
輸出的 err1
是(0x10ed120, 0x0
),也就是非空接口類型變量的類型信息并不為空,數(shù)據(jù)指針為空,因此它與 nil
(0x0, 0x0
)之間不能劃等號。
現(xiàn)在我們再回到我們開頭的那個問題,你是不是已經(jīng)豁然開朗了呢?開頭的問題中,從 returnsError
返回的 error
接口類型變量 err
的數(shù)據(jù)指針雖然為空,但它的類型信息(iface.tab
)并不為空,而是 *MyError
對應(yīng)的類型信息,這樣 err
與 nil
(0x0,0x0
)相比自然不相等,這就是我們開頭那個問題的答案解析,現(xià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)
}
這個示例的輸出結(jié)果如下:
eif: (0x10b3b00,0x10eb4d0)
err: (0x10ed380,0x10eb4d8)
eif = err: true
eif: (0x10b3b00,0x10eb4d0)
err: (0x10ed380,0x10eb4e0)
eif = err: false
你可以看到,空接口類型變量和非空接口類型變量內(nèi)部表示的結(jié)構(gòu)有所不同(第一個字段:_type
vs. tab
),兩者似乎一定不能相等。但 Go 在進行等值比較時,類型比較使用的是 eface
的 _type
和 iface
的 tab._type
,因此就像我們在這個例子中看到的那樣,當(dāng) eif
和 err
都被賦值為 T(5)
時,兩者之間是劃等號的。
好了,到這里,我們已經(jīng)理解了各類接口類型變量在運行時層的表示。我們可以通過 println
可以查看這個表示信息,從中我們也知道了接口變量只有在類型信息與值信息都一致的情況下才能劃等號。
五、輸出接口類型變量內(nèi)部表示的詳細信息
不過,println
輸出的接口類型變量的內(nèi)部表示信息,在一般情況下都是足夠的,但有些時候又顯得過于簡略,比如在上面最后一個例子中,如果僅憑 eif: (0x10b3b00,0x10eb4d0)
和 err: (0x10ed380,0x10eb4d8)
的輸出,我們是無法想到兩個變量是相等的。
那這時如果我們能輸出接口類型變量內(nèi)部表示的詳細信息(比如:tab._type
),那勢必可以取得事半功倍的效果。接下來我們就看看這要怎么做。
前面提到過,eface
和 iface
以及組成它們的 itab
和 _type
都是 runtime
包下的非導(dǎo)出結(jié)構(gòu)體,我們無法在外部直接引用它們。但我們發(fā)現(xiàn),組成 eface
、iface
的類型都是基本數(shù)據(jù)類型,我們完全可以通過“復(fù)制代碼”的方式將它們拿到 runtime
包外面來。
不過,這里要注意,由于 runtime
中的 eface
、iface
,或者它們的組成可能會隨著 Go 版本的變化發(fā)生變化,因此這個方法不具備跨版本兼容性。也就是說,基于 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)鍵部分,省略了部分代碼。上面這個 dumpinterface.go
中提供了三個主要函數(shù):
-
dumpEface
: 用于輸出空接口類型變量的內(nèi)部表示信息; -
dumpItabOfIface
: 用于輸出非空接口類型變量的tab
字段信息; -
dumpDataOfIface
: 用于輸出非空接口類型變量的data
字段信息;
我們利用這三個函數(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)
}
運行這個示例代碼,我們得到了這個輸出結(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
表達式的結(jié)果為 true
。
再次強調(diào)一遍,上面這個實現(xiàn)可能僅在 Go 1.17 版本上測試通過,并且在輸出 iface
或 eface
的 data
部分內(nèi)容時只列出了 int
、float64
和 T
類型的數(shù)據(jù)讀取實現(xiàn),沒有列出全部類型的實現(xiàn),你可以根據(jù)自己的需要實現(xiàn)其余數(shù)據(jù)類型。dumpinterface.go
的完整代碼你可以在這里找到。
我們現(xiàn)在已經(jīng)知道了,接口類型有著復(fù)雜的內(nèi)部結(jié)構(gòu),所以我們將一個類型變量值賦值給一個接口類型變量值的過程肯定不會像 var i int = 5
那么簡單,那么接口類型變量賦值的過程是怎樣的呢?其實接口類型變量賦值是一個“裝箱”的過程。
六、接口類型的裝箱(boxing)原理
裝箱(boxing)是編程語言領(lǐng)域的一個基礎(chǔ)概念,一般是指把一個值類型轉(zhuǎn)換成引用類型,比如在支持裝箱概念的 Java 語言中,將一個 int 變量轉(zhuǎn)換成 Integer 對象就是一個裝箱操作。
在 Go 語言中,將任意類型賦值給一個接口類型變量也是裝箱操作。有了前面對接口類型變量內(nèi)部表示的學(xué)習(xí),我們知道接口類型的裝箱實際就是創(chuàng)建一個 eface
或 iface
的過程。接下來我們就來簡要描述一下這個過程,也就是接口類型的裝箱原理。
我們基于下面這個例子中的接口裝箱操作來說明:
// 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)
}
這個例子中,對 ei
和 i
兩個接口類型變量的賦值都會觸發(fā)裝箱操作,要想知道 Go 在背后做了些什么,我們需要“下沉”一層,也就是要輸出上面 Go 代碼對應(yīng)的匯編代碼:
$go tool compile -S interface_internal.go > interface_internal.s
對應(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)
對應(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)
在將動態(tài)類型變量賦值給接口類型變量語句對應(yīng)的匯編代碼中,我們看到了 convT2E
和 convT2I
兩個 runtime
包的函數(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)換為一個 eface
,convT2I
用于將任意類型轉(zhuǎn)換為一個 iface
。兩個函數(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ù)與原變量便無瓜葛了,比如下面這個例子:
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ù)的類型信息是從何而來的呢?
其實這些都依賴 Go 編譯器的工作。編譯器知道每個要轉(zhuǎn)換為接口類型變量(toType)和動態(tài)類型變量的類型(fromType),它會根據(jù)這一對類型選擇適當(dāng)?shù)?convT2X
函數(shù),并在生成代碼時使用選出的 convT2X
函數(shù)參與裝箱操作。
不過,裝箱是一個有性能損耗的操作,因此 Go 也在不斷對裝箱操作進行優(yōu)化,包括對常見類型如整型、字符串、切片等提供系列快速轉(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 操作,增加了零值快速返回等特性。
同時 Go 建立了 staticuint64s
區(qū)域,對 255 以內(nèi)的小整數(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 語言中有著很高的地位。它這個地位的取得離不開它擁有的“動靜兼?zhèn)洹钡恼Z法特性。Go 接口的動態(tài)特性讓 Go 擁有與動態(tài)語言相近的靈活性,而靜態(tài)特性又在編譯階段保證了這種靈活性的安全。
要更好地理解 Go 接口的這兩種特性,我們需要深入到 Go 接口在運行時的表示層面上去。接口類型變量在運行時表示為 eface
和 iface
,eface
用于表示空接口類型變量,iface
用于表示非空接口類型變量。只有兩個接口類型變量的類型信息(eface._type
/iface.tab._type
)相同,且數(shù)據(jù)指針(eface.data
/iface.data
)所指數(shù)據(jù)相同時,兩個接口類型變量才是相等的。
我們可以通過 println
輸出接口類型變量的兩部分指針變量的值。而且,通過拷貝 runtime
包 eface
和 iface
相關(guān)類型源碼,我們還可以自定義輸出 eface
/iface
詳盡信息的函數(shù),不過要注意的是,由于 runtime
層代碼的演進,這個函數(shù)可能不具備在 Go 版本間的移植性。文章來源:http://www.zghlxwxcb.cn/news/detail-746043.html
最后,接口類型變量的賦值本質(zhì)上是一種裝箱操作,裝箱操作是由 Go 編譯器和運行時共同完成的,有一定的性能開銷,對于性能敏感的系統(tǒng)來說,我們應(yīng)該盡量避免或減少這類裝箱操作。文章來源地址http://www.zghlxwxcb.cn/news/detail-746043.html
到了這里,關(guān)于Go 接口:nil接口為什么不等于nil?的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!