什么是interface
在面向對象編程中,可以這么說:“接口定義了對象的行為”, 那么具體的實現行為就取決於對象了。
在Go中,接口是一組方法簽名(聲明的是一組方法的集合)。當一個類型為接口中的所有方法提供定義時,它被稱為實現該接口。它與oop非常相似。接口指定類型應具有的方法,類型決定如何實現這些方法。
讓我們來看看這個例子: Animal
類型是一個接口,我們將定義一個 Animal
作為任何可以說話的東西。這是 Go 類型系統的核心概念:我們根據類型可以執行的操作而不是其所能容納的數據類型來設計抽象。
type Animal interface { Speak() string }
Animal
為任何具有
Speak
方法的類型。
Speak
方法沒有參數,返回一個字符串。
所有定義了該方法的類型我們稱它實現了 Animal
接口。Go 中沒有
implements
關鍵字,判斷一個類型是否實現了一個接口是完全是自動地。讓我們創建幾個實現這個接口的類型:
type Dog struct { } func (d Dog) Speak() string { return "Woof!" } type Cat struct { } 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!" }
我們現在有四種不同類型的動物:
Dog
、
Cat
、
Llama
和
JavaProgrammer
。在我們的
main
函數中,我們創建了一個
[]Animal{Dog{}, Cat{}, Llama{}, JavaProgrammer{}}
,看看每只動物都說了些什么:
func main() { animals := []Animal{Dog{}, Cat{}, Llama{}, JavaProgrammer{}} for _, animal := range animals { fmt.Println(animal.Speak()) } }
interface{}
類型
interface{}
類型,
空接口,是導致很多混淆的根源。
interface{}
類型是沒有方法的接口。由於沒有
implements
關鍵字,所以所有類型都至少實現了 0 個方法,所以
所有類型都實現了空接口。這意味着,如果您編寫一個函數以
interface{}
值作為參數,那么您可以為該函數提供任何值。例如:
func DoSomething(v interface{}) { // ... }
DoSomething
函數內部,
v
的類型是什么?
新手們會認為 v
是任意類型的,但這是錯誤的。v
不是任意類型,它是 interface{}
類型。對的,沒錯!當將值傳遞給
DoSomething
函數時,Go 運行時將執行類型轉換(如果需要),並將值轉換為
interface{}
類型的值。所有值在運行時只有一個類型,而
v
的一個靜態類型是
interface{}
。
interface{}
的值呢?(具體到上例來說就是
[]Animal
中存的是啥?)
在我們上面的例子中,當我們初始化變量 animals
時,我們不需要像這樣 Animal(Dog{})
來顯示的轉型,因為這是自動地。這些元素都是 Animal
類型,但是他們的底層類型卻不相同。
為什么這很重要呢?理解接口是如何在內存中表示的,可以使得一些潛在的令人困惑的事情變得非常清楚。比如,像 “我可以將 []T 轉換為 []interface{}
嗎?” 這種問題就容易回答了。下面是一些爛代碼的例子,它們代表了對 interface{}
類型的常見誤解:
package main import ( "fmt" ) func PrintAll(vals []interface{}) { for _, val := range vals { fmt.Println(val) } } func main() { names := []string{"stanley", "david", "oscar"} PrintAll(names) }
運行這段代碼你會得到如下錯誤:cannot use names (type []string) as type []interface {} in argument to PrintAll
。如果想使其正常工作,我們必須將 []string
轉為 []interface{}
:
package main import ( "fmt" ) func PrintAll(vals []interface{}) { for _, val := range vals { fmt.Println(val) }
} func main() { names := []string{"stanley", "david", "oscar"} vals := make([]interface{}, len(names)) for i, v := range names { vals[i] = v } PrintAll(vals) }
很丑陋,但是生活就是這樣,沒有完美的事情。(事實上,這種情況不會經常發生,因為 []interface{}
並沒有像你想象的那樣有用)
指針和接口
Cat
的
Speak()
方法改為指針接收器:
func (c *Cat) Speak() string { return "Meow!" }
運行上述代碼,會得到如下錯誤:
cannot use Cat literal (type Cat) as type Animal in array or slice literal: Cat does not implement Animal (Speak method has pointer receiver)
該錯誤的意思是:你嘗試將 Cat
轉為 Animal
,但是只有 *Cat
類型實現了該接口。你可以通過傳入一個指針 (new(Cat)
或者 &Cat{}
)來修復這個錯誤。
animals := []Animal{Dog{}, new(Cat), Llama{}, JavaProgrammer{}}
讓我們做一些相反的事情:我們傳入一個 *Dog
指針,但是不改變 Dog
的 Speak()
方法:
animals := []Animal{new(Dog), new(Cat), Llama{}, JavaProgrammer{}}
這種方式可以正常工作,因為一個指針類型可以通過其相關的值類型來訪問值類型的方法,但是反過來不行。即,一個 *Dog
類型的值可以使用定義在 Dog
類型上的 Speak()
方法,而 Cat
類型的值不能訪問定義在 *Cat
類型上的方法。
這可能聽起來很神秘,但當你記住以下內容時就清楚了:Go 中的所有東西都是按值傳遞的。每次調用函數時,傳入的數據都會被復制。對於具有值接收者的方法,在調用該方法時將復制該值。例如下面的方法:
func (t T)MyMethod(s string) { // ... }
是 func(T, string)
類型的方法。方法接收器像其他參數一樣通過值傳遞給函數。
*Cat
的方法不能被
Cat
類型的值調用了。任何一個
Cat
類型的值可能會有很多
*Cat
類型的指針指向它,如果我們嘗試通過
Cat
類型的值來調用
*Cat
的方法,根本就不知道對應的是哪個指針。相反,如果
Dog
類型上有一個方法,通過
*Dog
來調用這個方法可以確切的找到該指針對應的
Gog
類型的值,從而調用上面的方法。運行時,Go 會自動幫我們做這些,所以我們不需要像 C語言中那樣使用類似如下的語句
d->Speak()
。
結語
我希望讀完此文后你可以更加得心應手地使用 Go 中的接口,記住下面這些結論:
- 通過考慮數據類型之間的相同功能來創建抽象,而不是相同字段
interface{}
的值不是任意類型,而是interface{}
類型- 接口包含兩個字的大小,類似於
(type, value)
- 函數可以接受
interface{}
作為參數,但最好不要返回interface{}
- 指針類型可以調用其所指向的值的方法,反過來不可以
- 函數中的參數甚至接受者都是通過值傳遞
- 一個接口的值就是就是接口而已,跟指針沒什么關系
- 如果你想在方法中修改指針所指向的值,使用
*
操作符