Go interface深入分析

1.鴨子類型(Duck Typing)

  • If it walks like a duck and it quacks like a duck, then it must be a duck.
  • interface是一種鴨子類型
  • 無需顯示聲明,只要對象實現了接口聲明的的全部方法,就實現了該接口
  • 把對象的類型檢查從編譯時推遲到運行時
  • 好處:
    • 鬆耦合
    • 可以先實現類型,再抽象接口

2.值receiver VS. 指針receiver

type T struct {}

func (t T) Value() {} //value receiver
func (t *T) Pointer() {} //pointer receiver
  • 值receiver會複製對象實例,而指針receiver不會
  • 把方法看作普通函數,receiver可以理解爲傳入的第一個參數
  • 只要receiver參數類型正確,方法就可以被執行

思考題:下面哪些語句在運行時會報錯?

func main() {
    var p *T

    p.Pointer()
    (*T)(nil).Pointer()
    (*T).Pointer(nil)
    p.Value()
}

另外,map中的元素是不可尋址的(not addressable),簡單來說就是不能取指針。所以如果map中存儲struct元素的話,大部分情況都是以指針類型定義的。

func main() {
    m := make(map[string]T, 0)
    m["a"] = T{}
    m["a"].Value() // GOOD
    m["a"].Pointer() // BAD,編譯錯誤
}
-----------------------------------------
func main() {
    m := make(map[string]*T, 0)
    m["a"] = T{}
    m["a"].Value() // GOOD
    m["a"].Pointer() // GOOD
}

3.方法集

  • 類型有一個與之相關的方法集,決定了它是否實現某個接口
  • 類型T的方法集包含所有receiver T的方法
  • 類型*T的方法集包含所有receiver T + *T的方法

可以通過反射進行驗證:

func printMethodSet(obj interface{}) {
    t := reflect.TypeOf(obj)

    for i, n := 0, t.NumMethod(); i < n; i++ {
        m := t.Method(i)
        fmt.Println(t, m.Name, m.Type)
    }
}

func main() {
    var t T

    printMethodSet(t)
    fmt.Println("----------------")
    printMethodSet(&t)
}

輸出結果:

main.T  Value func(main.T)
----------------
*main.T Pointer func(*main.T)
*main.T Value func(*main.T)

可以看到,*T類型包含了receiver T + *T的方法。但是,似乎Value()方法的receiver被改變了?

敲黑板:方法集僅僅用來驗證接口實現,對象或對象指針會直接調用原實現,不會使用方法集

思考題:下面程序的輸出是什麼?

type T struct {
    x int
}

func (t T) Value() { //value receiver
    t.x++
}
func (t *T) Pointer() { //pointer receiver
    t.x++  //Go沒有->運算符,編譯器會自動把t轉成(*t)
}

func main() {
    var t *T = &T{1}

    t.Value()
    fmt.Println(t.x)
    t.Pointer()
    fmt.Println(t.x)
}

4.什麼是interface?

先看一下Go語言的實現,代碼位於runtime/runtime2.go:

type iface struct {
        tab  *itab          //類型信息
        data unsafe.Pointer //實際對象指針
}

type itab struct {
        inter *interfacetype //接口類型
        _type *_type         //實際對象類型
        hash  uint32
        _     [4]byte
        fun   [1]uintptr     //實際對象方法地址
}

可以看到,interface其實就是兩個指針,一個指向類型信息,一個指向實際的對象。
在這裏插入圖片描述
對象方法查找的兩大陣營:

  • 靜態類型語言:如C++/Java,在編譯時生成完整的方法表
  • 動態類型語言:如Python/Javascript,在每次調用方法時進行查找(會使用cache)

Go採取了一種獨有(折衷)的實現方式:

  • 在進行類型轉換時計算itab,查找具體實現
  • itab類型只和interface相關,也就是說只包含接口聲明的方法的具體實現(沒有多餘方法)

舉例:

1 type I interface {
 2     hello()
 3 }   
 4 
 5 type S struct {
 6     x int
 7 }   
 8 func (S) hello() {}
 9 
10 func main() {
11     s := S{1}
12     var iter I = s
13     for i := 0; i < 100; i++ {
14         iter.hello()
15     }   
16 }

Go會在第12行完成itable的計算,然後在第14行直接跳轉。而在Python中則要到第14行才進行方法查找,雖然有cache的存在,仍然比直接一條跳轉指令低效得多。

5.interface賦值

  • 將對象賦值給接口變量時,會複製該對象
  • 把指針賦值給接口變量則不會發生複製操作

可以用gdb查看接口內部數據。先用下面的命令阻止編譯器優化:
go build -gcflags 「-N -l」

從下面的例子可以看出,s的地址和i.data不同,發生了對象複製:

type I interface {
    hello()
}

type S struct {
    x int
}
func (S) hello() {}

func main() {
    s := S{100}
    var i I = s
    i.hello()
}
================= gdb調試信息 =======================
(gdb) i locals
i = {tab = 0x1071dc0 <S,main.I>, data = 0xc420012098}
s = {x = 100}
(gdb) p/x &s
$1 = 0xc420041f58

而下面這個例子中是指針賦值,因此s的地址和i.data是相同的。

type I interface {
    hello()
}

type S struct {
    x int
}
func (*S) hello() {}

func main() {
    s := S{100}
    var i I = &s
    i.hello()
}
================= gdb調試信息 =======================
(gdb) i locals
&s = 0xc420076000
i = {tab = 0x1071cc0 <S,main.I>, data = 0xc420076000}

6.interface何時等於nil?

  • 只有當接口變量中的itab和data指針都爲nil時,接口才等於nil

常見錯誤:

type MyError struct{}

func (*MyError) Error() string {
    return "myerror"
}

func isPositive(x int) (int, error) {
    var err *MyError

    if (x <= 0) {
        err = new(MyError)
        return -x, err
    }

    return x, err //注意,err是有類型的!
}

func main() {
    _, err := isPositive(100)
    if err != nil {
        fmt.Println("ERROR!")
    }
}

可以看到,isPositive()函數返回err時相當於進行了一次類型轉換,把*MyError對象轉換爲一個error接口。這個接口變量的data指針爲nil,但itab指針不爲空,指向MyError類型。

正確做法:直接返回nil即可

7.空接口interface{}

  • interface{}可以接受任意類型,會自動進行轉換(類似於Java中的Object)
  • 例外:接口切片[]interface{}不會自動進行類型轉換

看下面的例子:

func print(names []interface{}) {
    for _, n := range names {
        fmt.Println(n)
    }
}

func main() {
    names := []string {"star", "jivin", "sheng"}
    print(names)
}

編譯後會報以下錯誤:

cannot use names (type []string) as type []interface {} in argument to print

原因解釋:[]interface{}在編譯時就有確定的內存佈局,每個元素的大小是固定的(2個指針),而[]string的內存佈局顯然不同。至於爲什麼Go爲什麼不幫我們做這個轉換,個人猜測可能是因爲轉換的開銷比較大。

解決方案1: 使用interface{}代替[]interface{}作爲參數

func print(names interface{}) {
    ns := names.([]string)
    for _, n := range ns {
        fmt.Println(n)
    }
}

解決方案2:手動做一次類型轉換

func main() {
    inames := make([]interface{}, len(names))
    for i, n := range names {
        inames[i] = n 
    }   

    print(inames)
}

參考:
https://github.com/golang/go/wiki/MethodSets
https://research.swtch.com/interfaces
https://github.com/golang/go/wiki/InterfaceSlice

更多文章歡迎關注「鑫鑫點燈」專欄:https://blog.csdn.net/turkeycock
或關注飛久微信公衆號:
在這裏插入圖片描述