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

Stefno發表於2019-04-25

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

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

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

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

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

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

翻譯過來就是:如果某個東西長得像鴨子,像鴨子一樣游泳,像鴨子一樣嘎嘎叫,那它就可以被看成是一隻鴨子。

Duck Typing,鴨子型別,是動態程式語言的一種物件推斷策略,它更關注物件能如何被使用,而不是物件的型別本身。Go 語言作為一門靜態語言,它通過通過介面的方式完美支援鴨子型別。

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

def hello_world(coder):
    coder.say_hello()

當呼叫此函式的時候,可以傳入任意型別,只要它實現了 say_hello() 函式就可以。如果沒有實現,執行過程中會出現錯誤。

而在靜態語言如 Java, C++ 中,必須要顯示地宣告實現了某個介面,之後,才能用在任何需要這個介面的地方。如果你在程式中呼叫 hello_world 函式,卻傳入了一個根本就沒有實現 say_hello() 的型別,那在編譯階段就不會通過。這也是靜態語言比動態語言更安全的原因。

動態語言和靜態語言的差別在此就有所體現。靜態語言在編譯期間就能發現型別不匹配的錯誤,不像動態語言,必須要執行到那一行程式碼才會報錯。插一句,這也是我不喜歡用 python 的一個原因。當然,靜態語言要求程式設計師在編碼階段就要按照規定來編寫程式,為每個變數規定資料型別,這在某種程度上,加大了工作量,也加長了程式碼量。動態語言則沒有這些要求,可以讓人更專注在業務上,程式碼也更短,寫起來更快,這一點,寫 python 的同學比較清楚。

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:24:6: cannot use Programmer literal (type Programmer) as type coder in assignment:
    Programmer 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" 來指定的。例如:

```C++ 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://user-images.githubuser ... 2e.png)

# 參考資料
【包含反射、介面等原始碼分析】https://zhuanlan.zhihu.com/p/27055513

【虛擬函式表和C++的區別】https://mp.weixin.qq.com/s/jU9HeR1tOyh-ME5iEYM5-Q

【具體型別向介面賦值】https://tiancaiamao.gitbooks.i ... .html

【Go夜讀群的討論】https://github.com/developer-l ... es.md

【廖雪峰 鴨子型別】https://www.liaoxuefeng.com/wi ... e3000

【值型別和指標型別,iface原始碼】https://www.jianshu.com/p/5f8ecbe4f6af

【總體說明itab的生成方式、作用】http://www.codeceo.com/article/go-interface.html

【conv系列函式的作用】https://blog.csdn.net/zhonglin ... 72336

【convI2I itab作用】https://www.jianshu.com/p/a5e99b1d50b1

【interface 原始碼解讀 很不錯 包含反射】http://wudaijun.com/2018/01/go ... ment/

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

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

【第一幅圖可以參考 gdb除錯】https://www.do1618.com/archive ... 2590/

【型別轉換和斷言】https://my.oschina.net/goal/blog/194308

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

【函式和方法】https://www.jianshu.com/p/5376e15966b3

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

【介面特點列表】https://segmentfault.com/a/1190000011451232

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

【Go四十二章經 interface】https://github.com/ffhelicopte ... ce.md

【對Go介面的反駁,有說到介面的定義】http://blog.zhaojie.me/2013/04 ... .html

【gopher 介面】http://fuxiaohei.me/2017/4/22/ ... .html

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

【infoQ 文章】https://www.infoq.cn/article/go-interface-talk

【Go介面詳解】https://zhuanlan.zhihu.com/p/27055513

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

【getitab原始碼說明】https://www.twblogs.net/a/5c245d59bd9eee16b3db561d

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

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

【探索C++與Go的介面底層實現】https://www.jianshu.com/p/073c09a05da7
https://github.com/teh-cmc/go- ... ME.md

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

【有圖】https://i6448038.github.io/201 ... face/

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

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

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

相關文章