Golang 之 interface接口全面理解


什么是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!"
}

  

我們現在有四種不同類型的動物: DogCatLlama 和  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)
    }
} 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{}
  • 指針類型可以調用其所指向的值的方法,反過來不可以
  • 函數中的參數甚至接受者都是通過值傳遞
  • 一個接口的值就是就是接口而已,跟指針沒什么關系
  • 如果你想在方法中修改指針所指向的值,使用 * 操作符


 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM