Golang 之 interface接口全面理解

什麼是interface


 

 

在面向對象編程中,能夠這麼說:「接口定義了對象的行爲」, 那麼具體的實現行爲就取決於對象了。編程

在Go中,接口是一組方法簽名(聲明的是一組方法的集合)。當一個類型爲接口中的全部方法提供定義時,它被稱爲實現該接口。它與oop很是類似。接口指定類型應具備的方法,類型決定如何實現這些方法。函數

 

讓咱們來看看這個例子: Animal 類型是一個接口,咱們將定義一個 Animal 做爲任何能夠說話的東西。這是 Go 類型系統的核心概念:咱們根據類型能夠執行的操做而不是其所能容納的數據類型來設計抽象。oop

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 類型,可是他們的底層類型卻不相同。spa

爲何這很重要呢?理解接口是如何在內存中表示的,可使得一些潛在的使人困惑的事情變得很是清楚。好比,像 「我能夠將 []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{} 並無像你想象的那樣有用)code

 

指針和接口

 
接口的另外一個微妙之處是接口定義沒有規定一個實現者是否應該使用一個指針接收器或一個值接收器來實現接口。當給定一個接口值時,不能保證底層類型是否爲指針。在前面的示例中,咱們將方法定義在值接收者之上。讓咱們稍微改變一下,將  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{})來修復這個錯誤。blog

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{}
  • 指針類型能夠調用其所指向的值的方法,反過來不能夠
  • 函數中的參數甚至接受者都是經過值傳遞
  • 一個接口的值就是就是接口而已,跟指針沒什麼關係
  • 若是你想在方法中修改指針所指向的值,使用 * 操做符
相關文章
相關標籤/搜索