理解 go interface 的 5 個關鍵點

LilHuondeKe 7年前發布 | 12K 次閱讀 Google Go/Golang開發 Golang Go

1、interface 是一種類型

type I interface {
    Get() int
}

首先 interface 是一種類型,從它的定義可以看出來用了 type 關鍵字,更準確的說 interface 是一種具有一組方法的類型,這些方法定義了 interface 的行為。

go 允許不帶任何方法的 interface ,這種類型的 interface 叫 empty interface。

如果一個類型實現了一個 interface 中所有方法,我們說類型實現了該 interface,所以所有類型都實現了 empty interface,因為任何一種類型至少實現了 0 個方法。go 沒有顯式的關鍵字用來實現 interface,只需要實現 interface 包含的方法即可。

2、interface 變量存儲的是實現者的值

//1
type I interface {    
    Get() int
    Set(int)
}

//2 type S struct {    Age int }

func(s S) Get()int {    return s.Age }

func(s *S) Set(age int) {    s.Age = age }

//3 func f(i I){    i.Set(10)    fmt.Println(i.Get()) }

func main() {    s := S{}    f(&s)  //4 }</code></pre>

這段代碼在 #1 定義了 interface I,在 #2 用 struct S 實現了 I 定義的兩個方法,接著在 #3 定義了一個函數 f 參數類型是 I,S 實現了 I 的兩個方法就說 S 是 I 的實現者,執行 f(&s) 就完了一次 interface 類型的使用。

interface 的重要用途就體現在函數 f 的參數中,如果有多種類型實現了某個 interface,這些類型的值都可以直接使用 interface 的變量存儲。

s := S{}
var i I //聲明 i 
i = &s //賦值 s 到 i
fmt.Println(i.Get())

不難看出 interface 的變量中存儲的是實現了 interface 的類型的對象值,這種能力是 duck typing。在使用 interface 時不需要顯式在 struct 上聲明要實現哪個 interface ,只需要實現對應 interface 中的方法即可,go 會自動進行 interface 的檢查,并在運行時執行從其他類型到 interface 的自動轉換,即使實現了多個 interface,go 也會在使用對應 interface 時實現自動轉換,這就是 interface 的魔力所在。

3、如何判斷 interface 變量存儲的是哪種類型

一個 interface 被多種類型實現時,有時候我們需要區分 interface 的變量究竟存儲哪種類型的值,go 可以使用 comma, ok 的形式做區分 value, ok := em.(T):em 是 interface 類型的變量,T代表要斷言的類型,value 是 interface 變量存儲的值,ok 是 bool 類型表示是否為該斷言的類型 T。

if t, ok := i.(*S); ok {
    fmt.Println("s implements I", t)
}

ok 是 true 表明 i 存儲的是 *S 類型的值,false 則不是,這種區分能力叫 Type assertions (類型斷言)。

如果需要區分多種類型,可以使用 switch 斷言,更簡單直接,這種斷言方式只能在 switch 語句中使用。

switch t := i.(type) {
case *S:
    fmt.Println("i store *S", t)
case *R:
    fmt.Println("i store *R", t)
}

4、空的 interface

interface{} 是一個空的 interface 類型,根據前文的定義:一個類型如果實現了一個 interface 的所有方法就說該類型實現了這個 interface,空的 interface 沒有方法,所以可以認為所有的類型都實現了 interface{}。如果定義一個函數參數是 interface{} 類型,這個函數應該可以接受任何類型作為它的參數。

func doSomething(v interface{}){    
}

如果函數的參數 v 可以接受任何類型,那么函數被調用時在函數內部 v 是不是表示的是任何類型?并不是,雖然函數的參數可以接受任何類型,并不表示 v 就是任何類型,在函數 doSomething 內部 v 僅僅是一個 interface 類型,之所以函數可以接受任何類型是在 go 執行時傳遞到函數的任何類型都被自動轉換成 interface{}。go 是如何進行轉換的,以及 v 存儲的值究竟是怎么做到可以接受任何類型的,感興趣的可以看看 Russ Cox 關于 interface 的實現 。

既然空的 interface 可以接受任何類型的參數,那么一個 interface{}類型的 slice 是不是就可以接受任何類型的 slice ?

func printAll(vals []interface{}) { //1
    for _, val := range vals {
        fmt.Println(val)
    }
}

func main(){ names := []string{"stanley", "david", "oscar"} printAll(names) }</code></pre>

上面的代碼是按照我們的假設修改的,執行之后竟然會報 

cannot use names (type []string) as type []interface {} in argument to printAll 錯誤,why?

這個錯誤說明 go 沒有幫助我們自動把 slice 轉換成 interface{} 類型的 slice,所以出錯了。go 不會對 類型是interface{} 的 slice 進行轉換 。為什么 go 不幫我們自動轉換,一開始我也很好奇,最后終于在 go 的 wiki 中找到了答案 https://github.com/golang/go/wiki/InterfaceSlice 大意是 interface{} 會占用兩個字長的存儲空間,一個是自身的 methods 數據,一個是指向其存儲值的指針,也就是 interface 變量存儲的值,因而 slice []interface{} 其長度是固定的N*2,但是 []T 的長度是N*sizeof(T),兩種 slice 實際存儲值的大小是有區別的(文中只介紹兩種 slice 的不同,至于為什么不能轉換猜測可能是 runtime 轉換代價比較大)。

但是我們可以手動進行轉換來達到我們的目的。

var dataSlice []int = foo()
var interfaceSlice []interface{} = make([]interface{}, len(dataSlice))
for i, d := range dataSlice {
    interfaceSlice[i] = d
}

5、interface 的實現者的 receiver 如何選擇

在我們上文的例子中調用 f 是 f(&s) 也就是 S 的指針類型,為什么不能是 f(s) 呢,如果是 s 會有什么問題?改成 f(s) 然后執行代碼。

cannot use s (type S) as type I in argument to f:
    S does not implement I (Set method has pointer receiver)

這個錯誤的意思是 S 沒有實現 I,哪里出了問題?關鍵點是 S 中 set 方法的 receiver 是個 pointer *S 。

interface 定義時并沒有嚴格規定實現者的方法 receiver 是個 value receiver 還是 pointer receiver,上面代碼中的 S 的 Set receiver 是 pointer,也就是實現 I 的兩個方法的 receiver 一個是 value 一個是 pointer,使用 f(s)的形勢調用,傳遞給 f 的是個 s 的一份拷貝,在進行 s 的拷貝到 I 的轉換時,s 的拷貝不滿足 Set 方法的 receiver 是個 pointer,也就沒有實現 I。go 中函數都是按值傳遞即 passed by value。

那反過來會怎樣,如果 receiver 是 value,函數用 pointer 的形式調用?

type I interface {
    Get() int
    Set(int)
}

type SS struct { Age int }

func (s SS) Get() int { return s.Age }

func (s SS) Set(age int) { s.Age = age }

func f(i I) { i.Set(10) fmt.Println(i.Get()) }

func main(){   ss := SS{} f(&ss) //ponter f(ss)  //value }</code></pre>

I 的實現者 SS 的方法 receiver 都是 value receiver,執行代碼可以看到無論是 pointer 還是 value 都可以正確執行。

導致這一現象的原因是什么?

如果是按 pointer 調用,go 會自動進行轉換,因為有了指針總是能得到指針指向的值是什么,如果是 value 調用,go 將無從得知 value 的原始值是什么,因為 value 是份拷貝。go 會把指針進行隱式轉換得到 value,但反過來則不行。

對于 receiver 是 value 的 method,任何在 method 內部對 value 做出的改變都不影響調用者看到的 value,這就是按值傳遞。

另一個說明上述現象的例子是這樣的來自 https://play.golang.org/p/TvR758rfre

package main

import ( "fmt" )

type Animal interface { Speak() string }

type Dog struct { }

func (d Dog) Speak() string { return "Woof!" }

type Cat struct { }

//1 func (c *Cat) Speak() string { return "Meow!" }

type Llama struct { }

func (l Llama) Speak() string { return "?????" }

type JavaProgrammer struct { }

func (j JavaProgrammer) Speak() string { return "Design patterns!" }

func main() { animals := []Animal{Dog{}, Cat{}, Llama{}, JavaProgrammer{}} for _, animal := range animals { fmt.Println(animal.Speak()) } }</code></pre>

#1 Cat 的 speak receiver 是 pointer,interface Animal 的 slice,Cat 的值是一個 value,同樣會因為 receiver 不一致而導致無法執行。

參考資料

  • https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/02.6.md

  • http://jordanorelli.com/post/32665860244/how-to-use-interfaces-in-go

  • https://tour.golang.org/methods/15

  • https://www.miek.nl/go/#interfaces

  • https://github.com/golang/go/wiki/InterfaceSlice

  • https://play.golang.org/p/TvR758rfre

  • https://golang.org/doc/effective_go.html#interfaces

  • http://en.wikipedia.org/wiki/Duck_typing

 

從本期開始我們將在一段時間內持續介紹有關學習 go 的過程中遇到的困惑和難點,歡迎關注討論。

 

點擊閱讀原文查看文章的最新更新和文中的所有代碼。

來自:https://mp.weixin.qq.com/s/lViWx3EFMOdIkijX7w7U-w

 

 本文由用戶 LilHuondeKe 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!