国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

Go 接口:nil接口為什么不等于nil?

這篇具有很好參考價值的文章主要介紹了Go 接口:nil接口為什么不等于nil?。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點擊"舉報違法"按鈕提交疑問。

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 來代表具有“會叫”這一特征的動物,而 DuckBirdDog 類型各自都具有這樣的特征,于是我們可以將這三個類型的變量賦值給 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)該是pnil,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)類型變量(如 intfloat64)那樣簡單,我們可以在 $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)部表示:ifaceeface,這兩種表示分別用于不同的接口類型變量:

  • 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) efaceiface 的結(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)。

而且,雖然 efaceiface 的第一個字段有所差別,但 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)部表示,這樣就可以一目了然地看出兩個變量是否相等了。

由于 efaceifaceruntime 包中的非導(dǎo)出結(jié)構(gòu)體定義,我們不能直接在包外使用,所以也就無法直接訪問到兩個結(jié)構(gòu)體中的數(shù)據(jù)。不過,Go 語言提供了 println 預(yù)定義函數(shù),可以用來輸出 efaceiface 的兩個指針字段的值。

在編譯階段,編譯器會根據(jù)要輸出的參數(shù)的類型將 println 替換為特定的函數(shù),這些函數(shù)都定義在 $GOROOT/src/runtime/print.go 文件中,而針對 efaceiface 類型的打印函數(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, ")")
}

我們看到,printefaceprintiface 會輸出各自的兩個指針字段的值。下面我們就來使用 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ù)值信息均為空。因此上面的變量 ierr 等值判斷為 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 行時,eif1eif2 已經(jīng)分別被賦值整型值 1718,這樣 eif1eif2 的動態(tài)類型的類型信息是相同的(都是 0x10ac580),但 data 指針指向的內(nèi)存塊中存儲的值不同,一個是 17,一個是 18,于是 eif1 不等于 eif2。

接著,代碼執(zhí)行到第 16 行的時候,eif2 已經(jīng)被重新賦值為 17,這樣 eif1eif2 不僅存儲的動態(tài)類型的類型信息是相同的(都是 0x10ac580),data 指針指向的內(nèi)存塊中存儲值也相同了,都是 17,于是 eif1 等于 eif2。

然后,代碼執(zhí)行到第 21 行時,eif2 已經(jīng)被重新賦值了 int64 類型的數(shù)值 17。這樣,eif1eif2 存儲的動態(tài)類型的類型信息就變成不同的了,一個是 int,一個是 int64,即便 data 指針指向的內(nèi)存塊中存儲值是相同的,最終 eif1eif2 也是不相等的。

第三種:非空接口類型變量

這里,我們也直接來看一個非空接口類型變量的內(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 輸出的 err1err2tabdata 值,要么 data 值不同,要么 tabdata 值都不同。

和空接口類型變量一樣,只有 tabdata 指的數(shù)據(jù)內(nèi)容一致的情況下,兩個非空接口類型變量之間才能劃等號。這里我們要注意 err1 下面的賦值情況:

err1 = (*T)(nil)

針對這種賦值,println 輸出的 err1 是(0x10ed120, 0x0),也就是非空接口類型變量的類型信息并不為空,數(shù)據(jù)指針為空,因此它與 nil0x0, 0x0)之間不能劃等號。

現(xiàn)在我們再回到我們開頭的那個問題,你是不是已經(jīng)豁然開朗了呢?開頭的問題中,從 returnsError 返回的 error 接口類型變量 err 的數(shù)據(jù)指針雖然為空,但它的類型信息(iface.tab)并不為空,而是 *MyError 對應(yīng)的類型信息,這樣 errnil0x0,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_typeifacetab._type,因此就像我們在這個例子中看到的那樣,當(dāng) eiferr 都被賦值為 T(5) 時,兩者之間是劃等號的。

好了,到這里,我們已經(jīng)理解了各類接口類型變量在運行時層的表示。我們可以通過 println 可以查看這個表示信息,從中我們也知道了接口變量只有在類型信息與值信息都一致的情況下才能劃等號。

五、輸出接口類型變量內(nèi)部表示的詳細信息

不過,println 輸出的接口類型變量的內(nèi)部表示信息,在一般情況下都是足夠的,但有些時候又顯得過于簡略,比如在上面最后一個例子中,如果僅憑 eif: (0x10b3b00,0x10eb4d0)err: (0x10ed380,0x10eb4d8) 的輸出,我們是無法想到兩個變量是相等的。

那這時如果我們能輸出接口類型變量內(nèi)部表示的詳細信息(比如:tab._type),那勢必可以取得事半功倍的效果。接下來我們就看看這要怎么做。

前面提到過,efaceiface 以及組成它們的 itab_type 都是 runtime 包下的非導(dǎo)出結(jié)構(gòu)體,我們無法在外部直接引用它們。但我們發(fā)現(xiàn),組成 eface、iface 的類型都是基本數(shù)據(jù)類型,我們完全可以通過“復(fù)制代碼”的方式將它們拿到 runtime 包外面來。

不過,這里要注意,由于 runtime 中的 efaceiface,或者它們的組成可能會隨著 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_type0x10b38c0)與 errtab._type0x10b38c0)是一致的,data 指針?biāo)竷?nèi)容(“bad error”)也是一致的,因此 eif == err 表達式的結(jié)果為 true。

再次強調(diào)一遍,上面這個實現(xiàn)可能僅在 Go 1.17 版本上測試通過,并且在輸出 ifaceefacedata 部分內(nèi)容時只列出了 int、float64T 類型的數(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)建一個 efaceiface 的過程。接下來我們就來簡要描述一下這個過程,也就是接口類型的裝箱原理。

我們基于下面這個例子中的接口裝箱操作來說明:

// 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)
  }

這個例子中,對 eii 兩個接口類型變量的賦值都會觸發(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)的匯編代碼中,我們看到了 convT2EconvT2I 兩個 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_typeconvT2Itab._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
}

那么 convT2EconvT2I 函數(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 接口在運行時的表示層面上去。接口類型變量在運行時表示為 efaceiface,eface 用于表示空接口類型變量,iface 用于表示非空接口類型變量。只有兩個接口類型變量的類型信息(eface._type/iface.tab._type)相同,且數(shù)據(jù)指針(eface.data/iface.data)所指數(shù)據(jù)相同時,兩個接口類型變量才是相等的。

我們可以通過 println 輸出接口類型變量的兩部分指針變量的值。而且,通過拷貝 runtimeefaceiface 相關(guān)類型源碼,我們還可以自定義輸出 eface/iface 詳盡信息的函數(shù),不過要注意的是,由于 runtime 層代碼的演進,這個函數(shù)可能不具備在 Go 版本間的移植性。

最后,接口類型變量的賦值本質(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)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實不符,請點擊違法舉報進行投訴反饋,一經(jīng)查實,立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費用

相關(guān)文章

  • 標(biāo)準化拉普拉斯矩陣特征值范圍為什么小于等于2?(證明)

    譜圖使用標(biāo)準化拉普拉斯矩陣 L n o r m L^{norm} L n or m 的一個重要原因就是, L n o r m L^{norm} L n or m 比拉普拉斯矩陣 L L L 穩(wěn)定。很多資料只是簡單地介紹了 L n o r m L^{norm} L n or m ,在kipfGCN中也只是簡單地提到 L n o r m L^{norm} L n or m 的特征值不大于2。本文搜集了相關(guān)lecture,并推導(dǎo)

    2024年02月11日
    瀏覽(78)
  • 為什么特征值的重數(shù)大于等于線性無關(guān)特征向量的個數(shù)

    為什么特征值的重數(shù)大于等于線性無關(guān)特征向量的個數(shù)

    關(guān)系就是,特征值的重數(shù) ≥ 該特征值的線性無關(guān)向量的個數(shù) ≥ 1 量化關(guān)系有 特征值的重數(shù),稱為 代數(shù)重數(shù) ,等于Jordan矩陣中特征值為λ的Jordan塊的階數(shù)之和 特征向量的個數(shù),稱為 幾何重數(shù) ,等于Jordan矩陣中特征值為λ的Jordan塊的個數(shù) 證明 先說結(jié)論 每個矩陣 等價 于一個

    2024年02月11日
    瀏覽(89)
  • C++ 為什么double類型不能直接判斷等于0 兩個double類型怎么判斷相等

    精度丟失, 十進制小數(shù)部分在轉(zhuǎn)換成2進制的時候經(jīng)常會出現(xiàn)無限位的二進制小數(shù),計算機存儲小數(shù)有長度限制,所以會進行截取部分小數(shù)進行存儲,計算機只能存儲大概的值,而不是精確的值 。 例如: 判斷一個單精度浮點數(shù):則是 if( abs(f) = 1e-6); 要判斷一個雙精度浮點數(shù)

    2024年02月12日
    瀏覽(104)
  • 為什么大廠都選擇用Go

    為什么大廠都選擇用Go

    字節(jié)跳動正式宣布開源CloudWeGo,這是一套以Go 語言為核心中間件集合。字節(jié)相關(guān)技術(shù)負責(zé)人表示希望CloudWeGo能豐富云原生社區(qū)的Golang工具體系。 可以看出,字節(jié)內(nèi)部已有諸多用Go語言開發(fā)的成熟項目,基本上也已經(jīng)全員轉(zhuǎn)Go了。其實不只是字節(jié),阿里、騰訊、百度、B站等也都

    2023年04月08日
    瀏覽(24)
  • Go 語言為什么很少使用數(shù)組?

    大家好,我是 frank,「Golang 語言開發(fā)?!构娞栕髡摺?01 介紹 在 Go 語言中,數(shù)組是一塊連續(xù)的內(nèi)存,數(shù)組不可以擴容,數(shù)組在作為參數(shù)傳遞時,屬于值傳遞。 數(shù)組的長度和類型共同決定數(shù)組的類型,不同類型的數(shù)組之間不可以比較,否則在編譯時會報錯。 因為數(shù)組的一些

    2024年02月04日
    瀏覽(21)
  • 為什么這么設(shè)計—— Go的GC

    Go語言采用了3色標(biāo)記清理法來對內(nèi)存進行自動垃圾回收, 過程是這樣的: (1)起初所有的對象都是白色的; (2)從根對象出發(fā)掃描所有可達對象,標(biāo)記為灰色,放入待處理隊列; (3)從待處理隊列中取出灰色對象,將其引用的對象標(biāo)記為灰色并放入待處理隊列中,自身標(biāo)

    2024年02月12日
    瀏覽(21)
  • 為什么不用Go開發(fā)操作系統(tǒng)?

    為什么不用Go開發(fā)操作系統(tǒng)?

    ? 操作系統(tǒng) (OS) 是計算機系統(tǒng)的心臟和靈魂,它管理著計算機的硬件和軟件資源,并為用戶提供與計算機交互的方式。傳統(tǒng)上,C 和 Assembly 等語言因其低開銷和 “接近機器碼” 的特性而被用于開發(fā)操作系統(tǒng)。 但諸如 Go 等高級語言的興起引入了一些特性,這些特性或許可以使

    2024年02月06日
    瀏覽(38)
  • 深入理解 go reflect - 反射為什么慢

    我們選擇 go 語言的一個重要原因是,它有非常高的性能。但是它反射的性能卻一直為人所詬病,本篇文章就來看看 go 反射的性能問題。 在開始之前,有必要先了解一下 go 的性能測試。在 go 里面進行性能測試很簡單,只需要在測試函數(shù)前面加上 Benchmark 前綴, 然后在函數(shù)體

    2024年02月01日
    瀏覽(34)
  • 為什么我們需要API接口?API接口的核心又是什么?
  • 為什么字節(jié)大量用GO而不是Java?

    為什么字節(jié)大量用GO而不是Java?

    見字 如面,我是軍哥。 我看很多程序員對字節(jié)編程語言選型很好奇,為此我還特地問了在字節(jié)的兩位4-1的技術(shù)大佬朋友,然后加上自己的思考,總結(jié)了一下就以下 2 個原因: 1、 選型上沒有歷史包袱 字節(jié)的早期的程序員大多來自于百度、360,本身就是 php / c++ 的背景,一開

    2024年02月08日
    瀏覽(24)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包