深度解密Go語言之關於 interface 的10個問題

此次文章依然很長,基本上涵蓋了 interface 的方方面面,有例子,有源碼分析,有彙編分析,前先後後寫了 20 多天。洋洋灑灑,長篇大論,依然有些東西沒有涉及到,好比文章裏沒有寫到反射,固然,後面會單獨寫一篇關於反射的文章,這是後話。php

仍是但願看你在看完文章後能有所收穫,有任何問題或意見建議,歡迎在文章後面留言。html

這篇文章的架構比較簡單,直接拋出 10 個問題,一一解答。python

1. Go 語言與鴨子類型的關係

先直接來看維基百科裏的定義:git

If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.

翻譯過來就是:若是某個東西長得像鴨子,像鴨子同樣游泳,像鴨子同樣嘎嘎叫,那它就能夠被當作是一隻鴨子。程序員

Duck Typing,鴨子類型,是動態編程語言的一種對象推斷策略,它更關注對象能如何被使用,而不是對象的類型自己。Go 語言做爲一門靜態語言,它經過經過接口的方式完美支持鴨子類型。github

例如,在動態語言 python 中,定義一個這樣的函數:golang

def hello_world(coder):
    coder.say_hello()

當調用此函數的時候,能夠傳入任意類型,只要它實現了 say_hello() 函數就能夠。若是沒有實現,運行過程當中會出現錯誤。shell

而在靜態語言如 Java, C++ 中,必需要顯示地聲明實現了某個接口,以後,才能用在任何須要這個接口的地方。若是你在程序中調用 hello_world 函數,卻傳入了一個根本就沒有實現 say_hello() 的類型,那在編譯階段就不會經過。這也是靜態語言比動態語言更安全的緣由。編程

動態語言和靜態語言的差異在此就有所體現。靜態語言在編譯期間就能發現類型不匹配的錯誤,不像動態語言,必需要運行到那一行代碼纔會報錯。插一句,這也是我不喜歡用 python 的一個緣由。固然,靜態語言要求程序員在編碼階段就要按照規定來編寫程序,爲每一個變量規定數據類型,這在某種程度上,加大了工做量,也加長了代碼量。動態語言則沒有這些要求,可讓人更專一在業務上,代碼也更短,寫起來更快,這一點,寫 python 的同窗比較清楚。segmentfault

Go 語言做爲一門現代靜態語言,是有後發優點的。它引入了動態語言的便利,同時又會進行靜態語言的類型檢查,寫起來是很是 Happy 的。Go 採用了折中的作法:不要求類型顯示地聲明實現了某個接口,只要實現了相關的方法便可,編譯器就能檢測到。

來看個例子:

先定義一個接口,和使用此接口做爲參數的函數:

type IGreeting interface {
    sayHello()
}

func sayHello(i IGreeting) {
    i.sayHello()
}

再來定義兩個結構體:

type Go struct {}
func (g Go) sayHello() {
    fmt.Println("Hi, I am GO!")
}

type PHP struct {}
func (p PHP) sayHello() {
    fmt.Println("Hi, I am PHP!")
}

最後,在 main 函數裏調用 sayHello() 函數:

func main() {
    golang := Go{}
    php := PHP{}

    sayHello(golang)
    sayHello(php)
}

程序輸出:

Hi, I am GO!
Hi, I am PHP!

在 main 函數中,調用調用 sayHello() 函數時,傳入了 golang, php 對象,它們並無顯式地聲明實現了 IGreeting 類型,只是實現了接口所規定的 sayHello() 函數。實際上,編譯器在調用 sayHello() 函數時,會隱式地將 golang, php 對象轉換成 IGreeting 類型,這也是靜態語言的類型檢查功能。

順帶再提一下動態語言的特色:

變量綁定的類型是不肯定的,在運行期間才能肯定
函數和方法能夠接收任何類型的參數,且調用時不檢查參數類型
不須要實現接口

總結一下,鴨子類型是一種動態語言的風格,在這種風格中,一個對象有效的語義,不是由繼承自特定的類或實現特定的接口,而是由它"當前方法和屬性的集合"決定。Go 做爲一種靜態語言,經過接口實現了 鴨子類型,其實是 Go 的編譯器在其中做了隱匿的轉換工做。

2. 值接收者和指針接收者的區別

方法

方法能給用戶自定義的類型添加新的行爲。它和函數的區別在於方法有一個接收者,給一個函數添加一個接收者,那麼它就變成了方法。接收者能夠是值接收者,也能夠是指針接收者

在調用方法的時候,值類型既能夠調用值接收者的方法,也能夠調用指針接收者的方法;指針類型既能夠調用指針接收者的方法,也能夠調用值接收者的方法。

也就是說,無論方法的接收者是什麼類型,該類型的值和指針均可以調用,沒必要嚴格符合接收者的類型。

來看個例子:

package main

import "fmt"

type Person struct {
    age int
}

func (p Person) howOld() int {
    return p.age
}

func (p *Person) growUp() {
    p.age += 1
}

func main() {
    // qcrao 是值類型
    qcrao := Person{age: 18}

    // 值類型 調用接收者也是值類型的方法
    fmt.Println(qcrao.howOld())

    // 值類型 調用接收者是指針類型的方法
    qcrao.growUp()
    fmt.Println(qcrao.howOld())

    // ----------------------

    // stefno 是指針類型
    stefno := &Person{age: 100}

    // 指針類型 調用接收者是值類型的方法
    fmt.Println(stefno.howOld())

    // 指針類型 調用接收者也是指針類型的方法
    stefno.growUp()
    fmt.Println(stefno.howOld())
}

上例子的輸出結果是:

18
19
100
101

調用了 growUp 函數後,無論調用者是值類型仍是指針類型,它的 Age 值都改變了。

實際上,當類型和方法的接收者類型不一樣時,實際上是編譯器在背後作了一些工做,用一個表格來呈現:

- 值接收者 指針接收者
值類型調用者 方法會使用調用者的一個副本,相似於「傳值」 使用值的引用來調用方法,上例中,qcrao.growUp() 其實是 (&qcrao).growUp()
指針類型調用者 指針被解引用爲值,上例中,stefno.howOld() 其實是 (*stefno).howOld() 實際上也是「傳值」,方法裏的操做會影響到調用者,相似於指針傳參,拷貝了一份指針

值接收者和指針接收者

前面說過,無論接收者類型是值類型仍是指針類型,均可以經過值類型或指針類型調用,這裏面實際上經過語法糖起做用的。

先說結論:實現了接收者是值類型的方法,至關於自動實現了接收者是指針類型的方法;而實現了接收者是指針類型的方法,不會自動生成對應接收者是值類型的方法。

來看一個例子,就會徹底明白:

package main

import "fmt"

type coder interface {
    code()
    debug()
}

type Gopher struct {
    language string
}

func (p Gopher) code() {
    fmt.Printf("I am coding %s language\n", p.language)
}

func (p *Gopher) debug() {
    fmt.Printf("I am debuging %s language\n", p.language)
}

func main() {
    var c coder = &Gopher{"Go"}
    c.code()
    c.debug()
}

上述代碼裏定義了一個接口 coder,接口定義了兩個函數:

code()
debug()

接着定義了一個結構體 Gopher,它實現了兩個方法,一個值接收者,一個指針接收者。

最後,咱們在 main 函數裏經過接口類型的變量調用了定義的兩個函數。

運行一下,結果:

I am coding Go language
I am debuging Go language

可是若是咱們把 main 函數的第一條語句換一下:

func main() {
    var c coder = Gopher{"Go"}
    c.code()
    c.debug()
}

運行一下,報錯:

./main.go:23:6: cannot use Gopher literal (type Gopher) as type coder in assignment:
    Gopher does not implement coder (debug method has pointer receiver)

看出這兩處代碼的差異了嗎?第一次是將 &Gopher 賦給了 coder;第二次則是將 Gopher 賦給了 coder

第二次報錯是說,Gopher 沒有實現 coder。很明顯了吧,由於 Gopher 類型並無實現 debug 方法;表面上看, *Gopher 類型也沒有實現 code 方法,可是由於 Gopher 類型實現了 code 方法,因此讓 *Gopher 類型自動擁有了 code 方法。

固然,上面的說法有一個簡單的解釋:接收者是指針類型的方法,極可能在方法中會對接收者的屬性進行更改操做,從而影響接收者;而對於接收者是值類型的方法,在方法中不會對接收者自己產生影響。

因此,當實現了一個接收者是值類型的方法,就能夠自動生成一個接收者是對應指針類型的方法,由於二者都不會影響接收者。可是,當實現了一個接收者是指針類型的方法,若是此時自動生成一個接收者是值類型的方法,本來指望對接收者的改變(經過指針實現),如今沒法實現,由於值類型會產生一個拷貝,不會真正影響調用者。

最後,只要記住下面這點就能夠了:

若是實現了接收者是值類型的方法,會隱含地也實現了接收者是指針類型的方法。

二者分別在什麼時候使用

若是方法的接收者是值類型,不管調用者是對象仍是對象指針,修改的都是對象的副本,不影響調用者;若是方法的接收者是指針類型,則調用者修改的是指針指向的對象自己。

使用指針做爲方法的接收者的理由:

  • 方法可以修改接收者指向的值。
  • 避免在每次調用方法時複製該值,在值的類型爲大型結構體時,這樣作會更加高效。

是使用值接收者仍是指針接收者,不是由該方法是否修改了調用者(也就是接收者)來決定,而是應該基於該類型的本質

若是類型具有「原始的本質」,也就是說它的成員都是由 Go 語言裏內置的原始類型,如字符串,整型值等,那就定義值接收者類型的方法。像內置的引用類型,如 slice,map,interface,channel,這些類型比較特殊,聲明他們的時候,其實是建立了一個 header, 對於他們也是直接定義值接收者類型的方法。這樣,調用函數時,是直接 copy 了這些類型的 header,而 header 自己就是爲複製設計的。

若是類型具有非原始的本質,不能被安全地複製,這種類型老是應該被共享,那就定義指針接收者的方法。好比 go 源碼裏的文件結構體(struct File)就不該該被複制,應該只有一份實體

這一段說的比較繞,你們能夠去看《Go 語言實戰》5.3 那一節。

3. iface 和 eface 的區別是什麼

ifaceeface 都是 Go 中描述接口的底層結構體,區別在於 iface 描述的接口包含方法,而 eface 則是不包含任何方法的空接口:interface{}

從源碼層面看一下:

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

type itab struct {
    inter  *interfacetype
    _type  *_type
    link   *itab
    hash   uint32 // copy of _type.hash. Used for type switches.
    bad    bool   // type does not implement interface
    inhash bool   // has this itab been added to hash?
    unused [2]byte
    fun    [1]uintptr // variable sized
}

iface 內部維護兩個指針,tab 指向一個 itab 實體, 它表示接口的類型以及賦給這個接口的實體類型。data 則指向接口具體的值,通常而言是一個指向堆內存的指針。

再來仔細看一下 itab 結構體:_type 字段描述了實體的類型,包括內存對齊方式,大小等;inter 字段則描述了接口的類型。fun 字段放置和接口方法對應的具體數據類型的方法地址,實現接口調用方法的動態分派,通常在每次給接口賦值發生轉換時會更新此表,或者直接拿緩存的 itab。

這裏只會列出實體類型和接口相關的方法,實體類型的其餘方法並不會出如今這裏。若是你學過 C++ 的話,這裏能夠類比虛函數的概念。

另外,你可能會以爲奇怪,爲何 fun 數組的大小爲 1,要是接口定義了多個方法可怎麼辦?實際上,這裏存儲的是第一個方法的函數指針,若是有更多的方法,在它以後的內存空間裏繼續存儲。從彙編角度來看,經過增長地址就能獲取到這些函數指針,沒什麼影響。順便提一句,這些方法是按照函數名稱的字典序進行排列的。

再看一下 interfacetype 類型,它描述的是接口的類型:

type interfacetype struct {
    typ     _type
    pkgpath name
    mhdr    []imethod
}

能夠看到,它包裝了 _type 類型,_type 其實是描述 Go 語言中各類數據類型的結構體。咱們注意到,這裏還包含一個 mhdr 字段,表示接口所定義的函數列表, pkgpath 記錄定義了接口的包名。

這裏經過一張圖來看下 iface 結構體的全貌:

iface 結構體全景

接着來看一下 eface 的源碼:

type eface struct {
    _type *_type
    data  unsafe.Pointer
}

相比 ifaceeface 就比較簡單了。只維護了一個 _type 字段,表示空接口所承載的具體的實體類型。data 描述了具體的值。

eface 結構體全景

咱們來看個例子:

package main

import "fmt"

func main() {
    x := 200
    var any interface{} = x
    fmt.Println(any)

    g := Gopher{"Go"}
    var c coder = g
    fmt.Println(c)
}

type coder interface {
    code()
    debug()
}

type Gopher struct {
    language string
}

func (p Gopher) code() {
    fmt.Printf("I am coding %s language\n", p.language)
}

func (p Gopher) debug() {
    fmt.Printf("I am debuging %s language\n", p.language)
}

執行命令,打印出彙編語言:

go tool compile -S ./src/main.go

能夠看到,main 函數裏調用了兩個函數:

func convT2E64(t *_type, elem unsafe.Pointer) (e eface)
func convT2I(tab *itab, elem unsafe.Pointer) (i iface)

上面兩個函數的參數和 ifaceeface 結構體的字段是能夠聯繫起來的:兩個函數都是將參數組裝一下,造成最終的接口。

做爲補充,咱們最後再來看下 _type 結構體:

type _type struct {
    // 類型大小
    size       uintptr
    ptrdata    uintptr
    // 類型的 hash 值
    hash       uint32
    // 類型的 flag,和反射相關
    tflag      tflag
    // 內存對齊相關
    align      uint8
    fieldalign uint8
    // 類型的編號,有bool, slice, struct 等等等等
    kind       uint8
    alg        *typeAlg
    // gc 相關
    gcdata    *byte
    str       nameOff
    ptrToThis typeOff
}

Go 語言各類數據類型都是在 _type 字段的基礎上,增長一些額外的字段來進行管理的:

type arraytype struct {
    typ   _type
    elem  *_type
    slice *_type
    len   uintptr
}

type chantype struct {
    typ  _type
    elem *_type
    dir  uintptr
}

type slicetype struct {
    typ  _type
    elem *_type
}

type structtype struct {
    typ     _type
    pkgPath name
    fields  []structfield
}

這些數據類型的結構體定義,是反射實現的基礎。

4. 接口的動態類型和動態值

從源碼裏能夠看到:iface包含兩個字段:tab 是接口表指針,指向類型信息;data 是數據指針,則指向具體的數據。它們分別被稱爲動態類型動態值。而接口值包括動態類型動態值

【引伸1】接口類型和 nil 做比較

接口值的零值是指動態類型動態值都爲 nil。當僅且當這兩部分的值都爲 nil 的狀況下,這個接口值就纔會被認爲 接口值 == nil

來看個例子:

package main

import "fmt"

type Coder interface {
    code()
}

type Gopher struct {
    name string
}

func (g Gopher) code() {
    fmt.Printf("%s is coding\n", g.name)
}

func main() {
    var c Coder
    fmt.Println(c == nil)
    fmt.Printf("c: %T, %v\n", c, c)

    var g *Gopher
    fmt.Println(g == nil)

    c = g
    fmt.Println(c == nil)
    fmt.Printf("c: %T, %v\n", c, c)
}

輸出:

true
c: <nil>, <nil>
true
false
c: *main.Gopher, <nil>

一開始,c 的 動態類型和動態值都爲 nilg 也爲 nil,當把 g 賦值給 c 後,c 的動態類型變成了 *main.Gopher,僅管 c 的動態值仍爲 nil,可是當 cnil 做比較的時候,結果就是 false 了。

【引伸2】
來看一個例子,看一下它的輸出:

package main

import "fmt"

type MyError struct {}

func (i MyError) Error() string {
    return "MyError"
}

func main() {
    err := Process()
    fmt.Println(err)

    fmt.Println(err == nil)
}

func Process() error {
    var err *MyError = nil
    return err
}

函數運行結果:

<nil>
false

這裏先定義了一個 MyError 結構體,實現了 Error 函數,也就實現了 error 接口。Process 函數返回了一個 error 接口,這塊隱含了類型轉換。因此,雖然它的值是 nil,其實它的類型是 *MyError,最後和 nil 比較的時候,結果爲 false

【引伸3】如何打印出接口的動態類型和值?

直接看代碼:

package main

import (
    "unsafe"
    "fmt"
)

type iface struct {
    itab, data uintptr
}

func main() {
    var a interface{} = nil

    var b interface{} = (*int)(nil)

    x := 5
    var c interface{} = (*int)(&x)
    
    ia := *(*iface)(unsafe.Pointer(&a))
    ib := *(*iface)(unsafe.Pointer(&b))
    ic := *(*iface)(unsafe.Pointer(&c))

    fmt.Println(ia, ib, ic)

    fmt.Println(*(*int)(unsafe.Pointer(ic.data)))
}

代碼裏直接定義了一個 iface 結構體,用兩個指針來描述 itabdata,以後將 a, b, c 在內存中的內容強制解釋成咱們自定義的 iface。最後就能夠打印出動態類型和動態值的地址。

運行結果以下:

{0 0} {17426912 0} {17426912 842350714568}
5

a 的動態類型和動態值的地址均爲 0,也就是 nil;b 的動態類型和 c 的動態類型一致,都是 *int;最後,c 的動態值爲 5。

5. 編譯器自動檢測類型是否實現接口

常常看到一些開源庫裏會有一些相似下面這種奇怪的用法:

var _ io.Writer = (*myWriter)(nil)

這時候會有點懵,不知道做者想要幹什麼,實際上這就是此問題的答案。編譯器會由此檢查 *myWriter 類型是否實現了 io.Writer 接口。

來看一個例子:

package main

import "io"

type myWriter struct {

}

/*func (w myWriter) Write(p []byte) (n int, err error) {
    return
}*/

func main() {
    // 檢查 *myWriter 類型是否實現了 io.Writer 接口
    var _ io.Writer = (*myWriter)(nil)

    // 檢查 myWriter 類型是否實現了 io.Writer 接口
    var _ io.Writer = myWriter{}
}

註釋掉爲 myWriter 定義的 Write 函數後,運行程序:

src/main.go:14:6: cannot use (*myWriter)(nil) (type *myWriter) as type io.Writer in assignment:
    *myWriter does not implement io.Writer (missing Write method)
src/main.go:15:6: cannot use myWriter literal (type myWriter) as type io.Writer in assignment:
    myWriter does not implement io.Writer (missing Write method)

報錯信息:*myWriter/myWriter 未實現 io.Writer 接口,也就是未實現 Write 方法。

解除註釋後,運行程序不報錯。

實際上,上述賦值語句會發生隱式地類型轉換,在轉換的過程當中,編譯器會檢測等號右邊的類型是否實現了等號左邊接口所規定的函數。

總結一下,可經過在代碼中添加相似以下的代碼,用來檢測類型是否實現了接口:

var _ io.Writer = (*myWriter)(nil)
var _ io.Writer = myWriter{}

6. 接口的構造過程是怎樣的

咱們已經看過了 ifaceeface 的源碼,知道 iface 最重要的是 itab_type

爲了研究清楚接口是如何構造的,接下來我會拿起彙編的武器,還原背後的真相。

來看一個示例代碼:

package main

import "fmt"

type Person interface {
    growUp()
}

type Student struct {
    age int
}

func (p Student) growUp() {
    p.age += 1
    return
}

func main() {
    var qcrao = Person(Student{age: 18})

    fmt.Println(qcrao)
}

執行命令:

go tool compile -S main.go

獲得 main 函數的彙編代碼以下:

0x0000 00000 (./src/main.go:30) TEXT    "".main(SB), $80-0
0x0000 00000 (./src/main.go:30) MOVQ    (TLS), CX
0x0009 00009 (./src/main.go:30) CMPQ    SP, 16(CX)
0x000d 00013 (./src/main.go:30) JLS     157
0x0013 00019 (./src/main.go:30) SUBQ    $80, SP
0x0017 00023 (./src/main.go:30) MOVQ    BP, 72(SP)
0x001c 00028 (./src/main.go:30) LEAQ    72(SP), BP
0x0021 00033 (./src/main.go:30) FUNCDATA$0, gclocals·69c1753bd5f81501d95132d08af04464(SB)
0x0021 00033 (./src/main.go:30) FUNCDATA$1, gclocals·e226d4ae4a7cad8835311c6a4683c14f(SB)
0x0021 00033 (./src/main.go:31) MOVQ    $18, ""..autotmp_1+48(SP)
0x002a 00042 (./src/main.go:31) LEAQ    go.itab."".Student,"".Person(SB), AX
0x0031 00049 (./src/main.go:31) MOVQ    AX, (SP)
0x0035 00053 (./src/main.go:31) LEAQ    ""..autotmp_1+48(SP), AX
0x003a 00058 (./src/main.go:31) MOVQ    AX, 8(SP)
0x003f 00063 (./src/main.go:31) PCDATA  $0, $0
0x003f 00063 (./src/main.go:31) CALL    runtime.convT2I64(SB)
0x0044 00068 (./src/main.go:31) MOVQ    24(SP), AX
0x0049 00073 (./src/main.go:31) MOVQ    16(SP), CX
0x004e 00078 (./src/main.go:33) TESTQ   CX, CX
0x0051 00081 (./src/main.go:33) JEQ     87
0x0053 00083 (./src/main.go:33) MOVQ    8(CX), CX
0x0057 00087 (./src/main.go:33) MOVQ    $0, ""..autotmp_2+56(SP)
0x0060 00096 (./src/main.go:33) MOVQ    $0, ""..autotmp_2+64(SP)
0x0069 00105 (./src/main.go:33) MOVQ    CX, ""..autotmp_2+56(SP)
0x006e 00110 (./src/main.go:33) MOVQ    AX, ""..autotmp_2+64(SP)
0x0073 00115 (./src/main.go:33) LEAQ    ""..autotmp_2+56(SP), AX
0x0078 00120 (./src/main.go:33) MOVQ    AX, (SP)
0x007c 00124 (./src/main.go:33) MOVQ    $1, 8(SP)
0x0085 00133 (./src/main.go:33) MOVQ    $1, 16(SP)
0x008e 00142 (./src/main.go:33) PCDATA  $0, $1
0x008e 00142 (./src/main.go:33) CALL    fmt.Println(SB)
0x0093 00147 (./src/main.go:34) MOVQ    72(SP), BP
0x0098 00152 (./src/main.go:34) ADDQ    $80, SP
0x009c 00156 (./src/main.go:34) RET
0x009d 00157 (./src/main.go:34) NOP
0x009d 00157 (./src/main.go:30) PCDATA  $0, $-1
0x009d 00157 (./src/main.go:30) CALL    runtime.morestack_noctxt(SB)
0x00a2 00162 (./src/main.go:30) JMP     0

咱們從第 10 行開始看,若是不理解前面幾行彙編代碼的話,能夠回去看看公衆號前面兩篇文章,這裏我就省略了。

彙編行數 操做
10-14 構造調用 runtime.convT2I64(SB) 的參數

咱們來看下這個函數的參數形式:

func convT2I64(tab *itab, elem unsafe.Pointer) (i iface) {
    // ……
}

convT2I64 會構造出一個 inteface,也就是咱們的 Person 接口。

第一個參數的位置是 (SP),這裏被賦上了 go.itab."".Student,"".Person(SB) 的地址。

咱們從生成的彙編找到:

go.itab."".Student,"".Person SNOPTRDATA dupok size=40
        0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  
        0x0010 00 00 00 00 00 00 00 00 da 9f 20 d4              
        rel 0+8 t=1 type."".Person+0
        rel 8+8 t=1 type."".Student+0

size=40 大小爲40字節,回顧一下:

type itab struct {
    inter  *interfacetype // 8字節
    _type  *_type // 8字節
    link   *itab // 8字節
    hash   uint32 // 4字節
    bad    bool   // 1字節
    inhash bool   // 1字節
    unused [2]byte // 2字節
    fun    [1]uintptr // variable sized // 8字節
}

把每一個字段的大小相加,itab 結構體的大小就是 40 字節。上面那一串數字其實是 itab 序列化後的內容,注意到大部分數字是 0,從 24 字節開始的 4 個字節 da 9f 20 d4 其實是 itabhash 值,這在判斷兩個類型是否相同的時候會用到。

下面兩行是連接指令,簡單說就是將全部源文件綜合起來,給每一個符號賦予一個全局的位置值。這裏的意思也比較明確:前8個字節最終存儲的是 type."".Person 的地址,對應 itab 裏的 inter 字段,表示接口類型;8-16 字節最終存儲的是 type."".Student 的地址,對應 itab_type 字段,表示具體類型。

第二個參數就比較簡單了,它就是數字 18 的地址,這也是初始化 Student 結構體的時候會用到。

彙編行數 操做
15 調用 runtime.convT2I64(SB)

具體看下代碼:

func convT2I64(tab *itab, elem unsafe.Pointer) (i iface) {
    t := tab._type
    
    //...
    
    var x unsafe.Pointer
    if *(*uint64)(elem) == 0 {
        x = unsafe.Pointer(&zeroVal[0])
    } else {
        x = mallocgc(8, t, false)
        *(*uint64)(x) = *(*uint64)(elem)
    }
    i.tab = tab
    i.data = x
    return
}

這塊代碼比較簡單,把 tab 賦給了 ifacetab 字段;data 部分則是在堆上申請了一塊內存,而後將 elem 指向的 18 拷貝過去。這樣 iface 就組裝好了。

彙編行數 操做
17 i.tab 賦給 CX
18 i.data 賦給 AX
19-21 檢測 i.tab 是不是 nil,若是不是的話,把 CX 移動 8 個字節,也就是把 itab_type 字段賦給了 CX,這也是接口的實體類型,最終要做爲 fmt.Println 函數的參數

後面,就是調用 fmt.Println 函數及以前的參數準備工做了,再也不贅述。

這樣,咱們就把一個 interface 的構造過程說完了。

【引伸1】
如何打印出接口類型的 Hash 值?

這裏參考曹大神翻譯的一篇文章,參考資料裏會寫上。具體作法以下:

type iface struct {
    tab  *itab
    data unsafe.Pointer
}
type itab struct {
    inter uintptr
    _type uintptr
    link uintptr
    hash  uint32
    _     [4]byte
    fun   [1]uintptr
}

func main() {
    var qcrao = Person(Student{age: 18})

    iface := (*iface)(unsafe.Pointer(&qcrao))
    fmt.Printf("iface.tab.hash = %#x\n", iface.tab.hash)
}

定義了一個山寨版ifaceitab,說它山寨是由於 itab 裏的一些關鍵數據結構都不具體展開了,好比 _type,對比一下正宗的定義就能夠發現,可是山寨版依然能工做,由於 _type 就是一個指針而已嘛。

main 函數裏,先構造出一個接口對象 qcrao,而後強制類型轉換,最後讀取出 hash 值,很是妙!你也能夠本身動手試一下。

運行結果:

iface.tab.hash = 0xd4209fda

值得一提的是,構造接口 qcrao 的時候,即便我把 age 寫成其餘值,獲得的 hash 值依然不變的,這應該是能夠預料的,hash 值只和他的字段、方法相關。

7. 類型轉換和斷言的區別

咱們知道,Go 語言中不容許隱式類型轉換,也就是說 = 兩邊,不容許出現類型不相同的變量。

類型轉換類型斷言本質都是把一個類型轉換成另一個類型。不一樣之處在於,類型斷言是對接口變量進行的操做。

類型轉換

對於類型轉換而言,轉換先後的兩個類型要相互兼容才行。類型轉換的語法爲:

<結果類型> := <目標類型> ( <表達式> )
package main

import "fmt"

func main() {
    var i int = 9

    var f float64
    f = float64(i)
    fmt.Printf("%T, %v\n", f, f)

    f = 10.8
    a := int(f)
    fmt.Printf("%T, %v\n", a, a)

    // s := []int(i)
}

上面的代碼裏,我定義了一個 int 型和 float64 型的變量,嘗試在它們以前相互轉換,結果是成功的:int 型和 float64 是相互兼容的。

若是我把最後一行代碼的註釋去掉,編譯器會報告類型不兼容的錯誤:

cannot convert i (type int) to type []int

斷言

前面說過,由於空接口 interface{} 沒有定義任何函數,所以 Go 中全部類型都實現了空接口。當一個函數的形參是 interface{},那麼在函數中,須要對形參進行斷言,從而獲得它的真實類型。

斷言的語法爲:

<目標類型的值>,<布爾參數> := <表達式>.( 目標類型 ) // 安全類型斷言
<目標類型的值> := <表達式>.( 目標類型 )  //非安全類型斷言

類型轉換和類型斷言有些類似,不一樣之處,在於類型斷言是對接口進行的操做。

仍是來看一個簡短的例子:

package main

import "fmt"

type Student struct {
    Name string
    Age int
}

func main() {
    var i interface{} = new(Student)
    s := i.(Student)
    
    fmt.Println(s)
}

運行一下:

panic: interface conversion: interface {} is *main.Student, not main.Student

直接 panic 了,這是由於 i*Student 類型,並不是 Student 類型,斷言失敗。這裏直接發生了 panic,線上代碼可能並不適合這樣作,能夠採用「安全斷言」的語法:

func main() {
    var i interface{} = new(Student)
    s, ok := i.(Student)
    if ok {
        fmt.Println(s)
    }
}

這樣,即便斷言失敗也不會 panic

斷言其實還有另外一種形式,就是用在利用 switch 語句判斷接口的類型。每個 case 會被順序地考慮。當命中一個 case 時,就會執行 case 中的語句,所以 case 語句的順序是很重要的,由於頗有可能會有多個 case 匹配的狀況。

代碼示例以下:

func main() {
    //var i interface{} = new(Student)
    //var i interface{} = (*Student)(nil)
    var i interface{}

    fmt.Printf("%p %v\n", &i, i)

    judge(i)
}

func judge(v interface{}) {
    fmt.Printf("%p %v\n", &v, v)

    switch v := v.(type) {
    case nil:
        fmt.Printf("%p %v\n", &v, v)
        fmt.Printf("nil type[%T] %v\n", v, v)

    case Student:
        fmt.Printf("%p %v\n", &v, v)
        fmt.Printf("Student type[%T] %v\n", v, v)

    case *Student:
        fmt.Printf("%p %v\n", &v, v)
        fmt.Printf("*Student type[%T] %v\n", v, v)

    default:
        fmt.Printf("%p %v\n", &v, v)
        fmt.Printf("unknow\n")
    }
}

type Student struct {
    Name string
    Age int
}

main 函數裏有三行不一樣的聲明,每次運行一行,註釋另外兩行,獲得三組運行結果:

// --- var i interface{} = new(Student)
0xc4200701b0 [Name: ], [Age: 0]
0xc4200701d0 [Name: ], [Age: 0]
0xc420080020 [Name: ], [Age: 0]
*Student type[*main.Student] [Name: ], [Age: 0]

// --- var i interface{} = (*Student)(nil)
0xc42000e1d0 <nil>
0xc42000e1f0 <nil>
0xc42000c030 <nil>
*Student type[*main.Student] <nil>

// --- var i interface{}
0xc42000e1d0 <nil>
0xc42000e1e0 <nil>
0xc42000e1f0 <nil>
nil type[<nil>] <nil>

對於第一行語句:

var i interface{} = new(Student)

i 是一個 *Student 類型,匹配上第三個 case,從打印的三個地址來看,這三處的變量實際上都是不同的。在 main 函數裏有一個局部變量 i;調用函數時,其實是複製了一份參數,所以函數裏又有一個變量 v,它是 i 的拷貝;斷言以後,又生成了一份新的拷貝。因此最終打印的三個變量的地址都不同。

對於第二行語句:

var i interface{} = (*Student)(nil)

這裏想說明的實際上是 i 在這裏動態類型是 (*Student), 數據爲 nil,它的類型並非 nil,它與 nil 做比較的時候,獲得的結果也是 false

最後一行語句:

var i interface{}

這回 i 纔是 nil 類型。

【引伸1】
fmt.Println 函數的參數是 interface。對於內置類型,函數內部會用窮舉法,得出它的真實類型,而後轉換爲字符串打印。而對於自定義類型,首先肯定該類型是否實現了 String() 方法,若是實現了,則直接打印輸出 String() 方法的結果;不然,會經過反射來遍歷對象的成員進行打印。

再來看一個簡短的例子,比較簡單,沒關係張:

package main

import "fmt"

type Student struct {
    Name string
    Age int
}

func main() {
    var s = Student{
        Name: "qcrao",
        Age: 18,
    }

    fmt.Println(s)
}

由於 Student 結構體沒有實現 String() 方法,因此 fmt.Println 會利用反射挨個打印成員變量:

{qcrao 18}

增長一個 String() 方法的實現:

func (s Student) String() string {
    return fmt.Sprintf("[Name: %s], [Age: %d]", s.Name, s.Age)
}

打印結果:

[Name: qcrao], [Age: 18]

按照咱們自定義的方法來打印了。

【引伸2】
針對上面的例子,若是改一下:

func (s *Student) String() string {
    return fmt.Sprintf("[Name: %s], [Age: %d]", s.Name, s.Age)
}

注意看兩個函數的接受者類型不一樣,如今 Student 結構體只有一個接受者類型爲 指針類型String() 函數,打印結果:

{qcrao 18}

爲何?

類型 T 只有接受者是 T 的方法;而類型 *T 擁有接受者是 T*T 的方法。語法上 T 能直接調 *T 的方法僅僅是 Go 的語法糖。

因此, Student 結構體定義了接受者類型是值類型的 String() 方法時,經過

fmt.Println(s)
fmt.Println(&s)

都可以按照自定義的格式來打印。

若是 Student 結構體定義了接受者類型是指針類型的 String() 方法時,只有經過

fmt.Println(&s)

才能按照自定義的格式打印。

8. 接口轉換的原理

經過前面提到的 iface 的源碼能夠看到,實際上它包含接口的類型 interfacetype 和 實體類型的類型 _type,這二者都是 iface 的字段 itab 的成員。也就是說生成一個 itab 同時須要接口的類型和實體的類型。

<interface 類型, 實體類型> ->itable

當斷定一種類型是否知足某個接口時,Go 使用類型的方法集和接口所須要的方法集進行匹配,若是類型的方法集徹底包含接口的方法集,則可認爲該類型實現了該接口。

例如某類型有 m 個方法,某接口有 n 個方法,則很容易知道這種斷定的時間複雜度爲 O(mn),Go 會對方法集的函數按照函數名的字典序進行排序,因此實際的時間複雜度爲 O(m+n)

這裏咱們來探索將一個接口轉換給另一個接口背後的原理,固然,能轉換的緣由必然是類型兼容。

直接來看一個例子:

package main

import "fmt"

type coder interface {
    code()
    run()
}

type runner interface {
    run()
}

type Gopher struct {
    language string
}

func (g Gopher) code() {
    return
}

func (g Gopher) run() {
    return
}

func main() {
    var c coder = Gopher{}

    var r runner
    r = c
    fmt.Println(c, r)
}

簡單解釋下上述代碼:定義了兩個 interface: coderrunner。定義了一個實體類型 Gopher,類型 Gopher 實現了兩個方法,分別是 run()code()。main 函數裏定義了一個接口變量 c,綁定了一個 Gopher 對象,以後將 c 賦值給另一個接口變量 r 。賦值成功的緣由是 c 中包含 run() 方法。這樣,兩個接口變量完成了轉換。

執行命令:

go tool compile -S ./src/main.go

獲得 main 函數的彙編命令,能夠看到: r = c 這一行語句其實是調用了 runtime.convI2I(SB),也就是 convI2I 函數,從函數名來看,就是將一個 interface 轉換成另一個 interface,看下它的源代碼:

func convI2I(inter *interfacetype, i iface) (r iface) {
    tab := i.tab
    if tab == nil {
        return
    }
    if tab.inter == inter {
        r.tab = tab
        r.data = i.data
        return
    }
    r.tab = getitab(inter, tab._type, false)
    r.data = i.data
    return
}

代碼比較簡單,函數參數 inter 表示接口類型,i 表示綁定了實體類型的接口,r 則表示接口轉換了以後的新的 iface。經過前面的分析,咱們又知道, iface 是由 tabdata 兩個字段組成。因此,實際上 convI2I 函數真正要作的事,找到新 interfacetabdata,就大功告成了。

咱們還知道,tab 是由接口類型 interfacetype 和 實體類型 _type。因此最關鍵的語句是 r.tab = getitab(inter, tab._type, false)

所以,重點來看下 getitab 函數的源碼,只看關鍵的地方:

func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
    // ……

    // 根據 inter, typ 計算出 hash 值
    h := itabhash(inter, typ)

    // look twice - once without lock, once with.
    // common case will be no lock contention.
    var m *itab
    var locked int
    for locked = 0; locked < 2; locked++ {
        if locked != 0 {
            lock(&ifaceLock)
        }
        
        // 遍歷哈希表的一個 slot
        for m = (*itab)(atomic.Loadp(unsafe.Pointer(&hash[h]))); m != nil; m = m.link {

            // 若是在 hash 表中已經找到了 itab(inter 和 typ 指針都相同)
            if m.inter == inter && m._type == typ {
                // ……
                
                if locked != 0 {
                    unlock(&ifaceLock)
                }
                return m
            }
        }
    }

    // 在 hash 表中沒有找到 itab,那麼新生成一個 itab
    m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys))
    m.inter = inter
    m._type = typ
    
    // 添加到全局的 hash 表中
    additab(m, true, canfail)
    unlock(&ifaceLock)
    if m.bad {
        return nil
    }
    return m
}

簡單總結一下:getitab 函數會根據 interfacetype_type 去全局的 itab 哈希表中查找,若是能找到,則直接返回;不然,會根據給定的 interfacetype_type 新生成一個 itab,並插入到 itab 哈希表,這樣下一次就能夠直接拿到 itab

這裏查找了兩次,而且第二次上鎖了,這是由於若是第一次沒找到,在第二次仍然沒有找到相應的 itab 的狀況下,須要新生成一個,而且寫入哈希表,所以須要加鎖。這樣,其餘協程在查找相同的 itab 而且也沒有找到時,第二次查找時,會被掛住,以後,就會查到第一個協程寫入哈希表的 itab

再來看一下 additab 函數的代碼:

// 檢查 _type 是否符合 interface_type 而且建立對應的 itab 結構體 將其放到 hash 表中
func additab(m *itab, locked, canfail bool) {
    inter := m.inter
    typ := m._type
    x := typ.uncommon()

    // both inter and typ have method sorted by name,
    // and interface names are unique,
    // so can iterate over both in lock step;
    // the loop is O(ni+nt) not O(ni*nt).
    // 
    // inter 和 typ 的方法都按方法名稱進行了排序
    // 而且方法名都是惟一的。因此循環的次數是固定的
    // 只用循環 O(ni+nt),而非 O(ni*nt)
    ni := len(inter.mhdr)
    nt := int(x.mcount)
    xmhdr := (*[1 << 16]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt]
    j := 0
    for k := 0; k < ni; k++ {
        i := &inter.mhdr[k]
        itype := inter.typ.typeOff(i.ityp)
        name := inter.typ.nameOff(i.name)
        iname := name.name()
        ipkg := name.pkgPath()
        if ipkg == "" {
            ipkg = inter.pkgpath.name()
        }
        for ; j < nt; j++ {
            t := &xmhdr[j]
            tname := typ.nameOff(t.name)
            // 檢查方法名字是否一致
            if typ.typeOff(t.mtyp) == itype && tname.name() == iname {
                pkgPath := tname.pkgPath()
                if pkgPath == "" {
                    pkgPath = typ.nameOff(x.pkgpath).name()
                }
                if tname.isExported() || pkgPath == ipkg {
                    if m != nil {
                        // 獲取函數地址,並加入到itab.fun數組中
                        ifn := typ.textOff(t.ifn)
                        *(*unsafe.Pointer)(add(unsafe.Pointer(&m.fun[0]), uintptr(k)*sys.PtrSize)) = ifn
                    }
                    goto nextimethod
                }
            }
        }
        // ……
        
        m.bad = true
        break
    nextimethod:
    }
    if !locked {
        throw("invalid itab locking")
    }

    // 計算 hash 值
    h := itabhash(inter, typ)
    // 加到Hash Slot鏈表中
    m.link = hash[h]
    m.inhash = true
    atomicstorep(unsafe.Pointer(&hash[h]), unsafe.Pointer(m))
}

additab 會檢查 itab 持有的 interfacetype_type 是否符合,就是看 _type 是否徹底實現了 interfacetype 的方法,也就是看二者的方法列表重疊的部分就是 interfacetype 所持有的方法列表。注意到其中有一個雙層循環,乍一看,循環次數是 ni * nt,但因爲二者的函數列表都按照函數名稱進行了排序,所以最終只執行了 ni + nt 次,代碼裏經過一個小技巧來實現:第二層循環並無從 0 開始計數,而是從上一次遍歷到的位置開始。

求 hash 值的函數比較簡單:

func itabhash(inter *interfacetype, typ *_type) uint32 {
    h := inter.typ.hash
    h += 17 * typ.hash
    return h % hashSize
}

hashSize 的值是 1009。

更通常的,當把實體類型賦值給接口的時候,會調用 conv 系列函數,例如空接口調用 convT2E 系列、非空接口調用 convT2I 系列。這些函數比較類似:

  1. 具體類型轉空接口時,_type 字段直接複製源類型的 _type;調用 mallocgc 得到一塊新內存,把值複製進去,data 再指向這塊新內存。
  2. 具體類型轉非空接口時,入參 tab 是編譯器在編譯階段預先生成好的,新接口 tab 字段直接指向入參 tab 指向的 itab;調用 mallocgc 得到一塊新內存,把值複製進去,data 再指向這塊新內存。
  3. 而對於接口轉接口,itab 調用 getitab 函數獲取。只用生成一次,以後直接從 hash 表中獲取。

9. 如何用 interface 實現多態

Go 語言並無設計諸如虛函數、純虛函數、繼承、多重繼承等概念,但它經過接口卻很是優雅地支持了面向對象的特性。

多態是一種運行期的行爲,它有如下幾個特色:

  1. 一種類型具備多種類型的能力
  2. 容許不一樣的對象對同一消息作出靈活的反應
  3. 以一種通用的方式對待個使用的對象
  4. 非動態語言必須經過繼承和接口的方式來實現

看一個實現了多態的代碼例子:

package main

import "fmt"

func main() {
    qcrao := Student{age: 18}
    whatJob(&qcrao)

    growUp(&qcrao)
    fmt.Println(qcrao)

    stefno := Programmer{age: 100}
    whatJob(stefno)

    growUp(stefno)
    fmt.Println(stefno)
}

func whatJob(p Person) {
    p.job()
}

func growUp(p Person) {
    p.growUp()
}

type Person interface {
    job()
    growUp()
}

type Student struct {
    age int
}

func (p Student) job() {
    fmt.Println("I am a student.")
    return
}

func (p *Student) growUp() {
    p.age += 1
    return
}

type Programmer struct {
    age int
}

func (p Programmer) job() {
    fmt.Println("I am a programmer.")
    return
}

func (p Programmer) growUp() {
    // 程序員老得太快 ^_^
    p.age += 10
    return
}

代碼裏先定義了 1 個 Person 接口,包含兩個函數:

job()
growUp()

而後,又定義了 2 個結構體,StudentProgrammer,同時,類型 *StudentProgrammer 實現了 Person 接口定義的兩個函數。注意,*Student 類型實現了接口, Student 類型卻沒有。

以後,我又定義了函數參數是 Person 接口的兩個函數:

func whatJob(p Person)
func growUp(p Person)

main 函數裏先生成 StudentProgrammer 的對象,再將它們分別傳入到函數 whatJobgrowUp。函數中,直接調用接口函數,實際執行的時候是看最終傳入的實體類型是什麼,調用的是實體類型實現的函數。因而,不一樣對象針對同一消息就有多種表現,多態就實現了。

更深刻一點來講的話,在函數 whatJob() 或者 growUp() 內部,接口 person 綁定了實體類型 *Student 或者 Programmer。根據前面分析的 iface 源碼,這裏會直接調用 fun 裏保存的函數,相似於: s.tab->fun[0],而由於 fun 數組裏保存的是實體類型實現的函數,因此當函數傳入不一樣的實體類型時,調用的其實是不一樣的函數實現,從而實現多態。

運行一下代碼:

I am a student.
{19}
I am a programmer.
{100}

10. Go 接口與 C++ 接口有何異同

接口定義了一種規範,描述了類的行爲和功能,而不作具體實現。

C++ 的接口是使用抽象類來實現的,若是類中至少有一個函數被聲明爲純虛函數,則這個類就是抽象類。純虛函數是經過在聲明中使用 "= 0" 來指定的。例如:

class Shape
{
   public:
      // 純虛函數
      virtual double getArea() = 0;
   private:
      string name;      // 名稱
};

設計抽象類的目的,是爲了給其餘類提供一個能夠繼承的適當的基類。抽象類不能被用於實例化對象,它只能做爲接口使用。

派生類須要明確地聲明它繼承自基類,而且須要實現基類中全部的純虛函數。

C++ 定義接口的方式稱爲「侵入式」,而 Go 採用的是 「非侵入式」,不須要顯式聲明,只須要實現接口定義的函數,編譯器自動會識別。

C++ 和 Go 在定義接口方式上的不一樣,也致使了底層實現上的不一樣。C++ 經過虛函數表來實現基類調用派生類的函數;而 Go 經過 itab 中的 fun 字段來實現接口變量調用實體類型的函數。C++ 中的虛函數表是在編譯期生成的;而 Go 的 itab 中的 fun 字段是在運行期間動態生成的。緣由在於,Go 中實體類型可能會無心中實現 N 多接口,不少接口並非原本須要的,因此不能爲類型實現的全部接口都生成一個 itab, 這也是「非侵入式」帶來的影響;這在 C++ 中是不存在的,由於派生須要顯示聲明它繼承自哪一個基類。

QR

參考資料

【包含反射、接口等源碼分析】https://zhuanlan.zhihu.com/p/...

【虛函數表和C++的區別】https://mp.weixin.qq.com/s/jU...

【具體類型向接口賦值】https://tiancaiamao.gitbooks....

【Go夜讀羣的討論】https://github.com/developer-...

【廖雪峯 鴨子類型】https://www.liaoxuefeng.com/w...

【值類型和指針類型,iface源碼】https://www.jianshu.com/p/5f8...

【整體說明itab的生成方式、做用】http://www.codeceo.com/articl...

【conv系列函數的做用】https://blog.csdn.net/zhongli...

【convI2I itab做用】https://www.jianshu.com/p/a5e...

【interface 源碼解讀 很不錯 包含反射】http://wudaijun.com/2018/01/g...

【what why how思路來寫interface】http://legendtkl.com/2017/06/...

【有彙編分析,不錯】http://legendtkl.com/2017/07/...

【第一幅圖能夠參考 gdb調試】https://www.do1618.com/archiv...

【類型轉換和斷言】https://my.oschina.net/goal/b...

【interface 和 nil】https://my.oschina.net/goal/b...

【函數和方法】https://www.jianshu.com/p/537...

【反射】https://flycode.co/archives/2...

【接口特色列表】https://segmentfault.com/a/11...

【interface 全面介紹,包含C++對比】https://www.jianshu.com/p/b38...

【Go四十二章經 interface】https://github.com/ffhelicopt...

【對Go接口的反駁,有說到接口的定義】http://blog.zhaojie.me/2013/0...

【gopher 接口】http://fuxiaohei.me/2017/4/22...

【譯文 還不錯】https://mp.weixin.qq.com/s/tB...

【infoQ 文章】https://www.infoq.cn/article/...

【Go接口詳解】https://zhuanlan.zhihu.com/p/...

【Go interface】https://sanyuesha.com/2017/07...

【getitab源碼說明】https://www.twblogs.net/a/5c2...

【淺顯易懂】https://yami.io/golang-interf...

【golang io包的妙用】https://www.jianshu.com/p/8c3...

【探索C++與Go的接口底層實現】https://www.jianshu.com/p/073...
https://github.com/teh-cmc/go...

【彙編層面】http://xargin.com/go-and-inte...

【有圖】https://i6448038.github.io/20...

【圖】https://mp.weixin.qq.com/s/px...

【英文開源書】https://github.com/cch123/go-...

【曹大的翻譯】http://xargin.com/go-and-inte...

相關文章
相關標籤/搜索