理解 Golang 中的 interface 和 interface{}
【導讀】本文介紹了 Go 語言的接口。
在面向對象編程中,可以這麼說:“接口定義了對象的行爲”, 那麼具體的實現行爲就取決於對象了。
在 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
中存的是啥?)
一個接口值由兩個字(32 位機器一個字是 32 bits,64 位機器一個字是 64 bits)組成;一個字用於指向該值底層類型的方法表,另一個字用於指向實際數據。我不想沒完沒了地談論這個。
在我們上面的例子中,當我們初始化變量 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)
}<br>
}
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{}
-
指針類型可以調用其所指向的值的方法,反過來不可以
-
函數中的參數甚至接受者都是通過值傳遞
-
一個接口的值就是就是接口而已,跟指針沒什麼關係
-
如果你想在方法中修改指針所指向的值,使用
*
操作符
轉自:
cnblogs.com/maji233/p/11178413.html
Go 開發大全
參與維護一個非常全面的 Go 開源技術資源庫。日常分享 Go, 雲原生、k8s、Docker 和微服務方面的技術文章和行業動態。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/qo18LZ2OzFS5w0Ybqa__3A