[譯] 如何在 Go 中使用接口

本文翻譯自 How to use interfaces in Go 有部分刪減,請以原文爲準程序員

在開始使用 Go 編程以前,個人大部分工做都是用 Python 完成的。做爲一名 Python 程序員,我發現學習使用 Go 中的接口是很是困難的。基礎很簡單,並且我知道如何在標準庫中使用接口,可是我作了不少練習以後才知道如何設計本身的接口。在本文中,我將討論 Go 的類型系統,以解釋如何有效地使用接口。golang

接口介紹

接口是什麼?一個接口包含兩層意思:它是一個方法的集合,一樣是一個類型。讓咱們首先關注接口做爲方法的集合這一方面。web

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

type Animal interface {
    Speak() string
}
複製代碼

很是簡單:咱們定義 Animal 爲任何具備 Speak 方法的類型。Speak 方法沒有參數,返回一個字符串。全部定義了該方法的類型咱們稱它實現Animal 接口。Go 中沒有 implements 關鍵字,判斷一個類型是否實現了一個接口是徹底是自動地。讓咱們建立幾個實現這個接口的類型:json

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!"
}
複製代碼

咱們如今有四種不一樣類型的動物:DogCatLlamaJavaProgrammer。在咱們的 main 函數中,咱們建立了一個 []Animal{Dog{}, Cat{}, Llama{}, JavaProgrammer{}} ,看看每隻動物都說了些什麼:安全

func main() {
    animals := []Animal{Dog{}, Cat{}, Llama{}, JavaProgrammer{}}
    for _, animal := range animals {
        fmt.Println(animal.Speak())
    }
}
複製代碼

很好,如今你知道如何使用接口了,我不須要再討論它們了,對吧?不是的。讓咱們來看看一些不太明顯的東西。bash

interface{} 類型

interface{} 類型,空接口,是致使不少混淆的根源。interface{} 類型是沒有方法的接口。因爲沒有 implements 關鍵字,因此全部類型都至少實現了 0 個方法,因此 全部類型都實現了空接口。這意味着,若是您編寫一個函數以 interface{} 值做爲參數,那麼您能夠爲該函數提供任何值。例如:函數

func DoSomething(v interface{}) {
   // ...
}
複製代碼

這裏是讓人困惑的地方:在 DoSomething 函數內部,v 的類型是什麼?新手們會認爲 v任意類型的,但這是錯誤的。v 不是任意類型,它是 interface{} 類型。對的,沒錯!當將值傳遞給DoSomething 函數時,Go 運行時將執行類型轉換(若是須要),並將值轉換爲 interface{} 類型的值。全部值在運行時只有一個類型,而 v 的一個靜態類型是 interface{}post

這可能讓您感到疑惑:好吧,若是發生了轉換,究竟是什麼東西傳入了函數做爲 interface{} 的值呢?(具體到上例來講就是 []Animal 中存的是啥?)學習

一個接口值由兩個字(32 位機器一個字是 32 bits,64 位機器一個字是 64 bits)組成;一個字用於指向該值底層類型的方法表,另外一個字用於指向實際數據。我不想沒完沒了地談論這個。若是您理解一個接口值是兩個字,而且它包含指向底層數據的指針,那麼這就足以免常見的陷阱。若是您想了解更多關於接口實現的知識。這篇文章頗有用:Russ Cox’s description of interfaces 。

在咱們上面的例子中,當咱們初始化變量 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{} 並無像你想象的那樣有用)

指針和接口

接口的另外一個微妙之處是接口定義沒有規定一個實現者是否應該使用一個指針接收器或一個值接收器來實現接口。當給定一個接口值時,不能保證底層類型是否爲指針。在前面的示例中,咱們將方法定義在值接收者之上。讓咱們稍微改變一下,將 CatSpeak() 方法改成指針接收器:

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 指針,可是不改變 DogSpeak() 方法:

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()

例1:經過 Twitter API 獲取正確的時間戳

Twitter API 使用下面的格式來展現時間戳:

"Thu May 31 00:00:01 +0000 2012"
複製代碼

Twitter API 返回的是一個 json 字符串,這裏咱們只考慮解析 created_at 字段:

package main

import (
    "encoding/json"
    "fmt"
    "reflect"
)

// start with a string representation of our JSON data
var input = `
{
    "created_at": "Thu May 31 00:00:01 +0000 2012"
}
`

func main() {
    // our target will be of type map[string]interface{}, which is a
    // pretty generic type that will give us a hashtable whose keys
    // are strings, and whose values are of type interface{}
    var val map[string]interface{}

    if err := json.Unmarshal([]byte(input), &val); err != nil {
        panic(err)
    }

    fmt.Println(val)
    for k, v := range val {
        fmt.Println(k, reflect.TypeOf(v))
    }
}
複製代碼

運行上述代碼,輸出:

map[created_at:Thu May 31 00:00:01 +0000 2012]
created_at Thu May 31 00:00:01 +0000 2012 string
複製代碼

咱們獲得瞭解析後的結果,可是解析出來的時間是字符串類型的,做用有限,所以咱們想把它解析成 time.Time 類型的,對代碼作出以下修改:

var val map[string]interface{} -> var val map[string]time.Time
複製代碼

結果出錯了:

panic: parsing time ""Thu May 31 00:00:01 +0000 2012"" as ""2006-01-02T15:04:05Z07:00"": cannot parse "Thu May 31 00:00:01 +0000 2012"" as "2006" 複製代碼

出錯的緣由是字符串格式與 Go 中的時間格式不匹配(由於 Twitter's API 是用 Ruby 寫的,其格式跟 Go 不一樣)。咱們必須定義咱們本身的類型來解析時間。encoding/json 在解析時會判斷傳入 json.Unmarshal 的值是否實現了 json.Unmarshaler 接口:

type Unmarshaler interface {
    UnmarshalJSON([]byte) error
}
複製代碼

若是實現了,就會調用 UnmarshalJSON 方法來解析(參考),因此咱們須要的是一個實現了 UnmarshalJSON([]byte) error 方法的類型:

type Timestamp time.Time

func (t *Timestamp) UnmarshalJSON(b []byte) error {
    // ...
}
複製代碼

值得注意的是,咱們使用一個指針做爲方法接受者,由於咱們但願在方法內對接受者進行更改。UnmarshalJSON 中,t 表明指向 Timestamp 類型值的指針,經過 *t 咱們能夠訪問到這個值,這樣就能夠修改它了。

咱們可使用 time.Parse(layout, value string) (Time, error) 來解析時間,該函數的第一個參數是表示時間格式的字符串(更多字符串格式),第二個是咱們要解析的字符串。返回 time.Time 類型的值以及 error(若是解析出錯)。解析獲得 time.Time 類型的值後,轉換成 Timestamp 類型而後賦值給 *t

func (t *Timestamp) UnmarshalJSON(b []byte) error {
    v, err := time.Parse(time.RubyDate, string(b[1:len(b)-1]))
    if err != nil {
        return err
    }
    *t = Timestamp(v)
    return nil
}
複製代碼

注意,傳入函數的 []byte 是原始的 JSON 數據,其中包含有引號,因此這裏須要切片去掉引號。

例2:從 HTTP 請求中獲得對象

然咱們設計一個接口來解決 web 開發中常見的一個問題:咱們想解析 HTTP 請求體獲得咱們須要的對象數據。例如,咱們這樣定義咱們的接口:

GetEntity(*http.Request) (interface{}, error)
複製代碼

由於 interface{} 能夠有任意的底層類型,因此咱們能夠解析獲得任何咱們須要的東西。可是這是一個很差的設計,咱們將過多的邏輯引入到 GetEntity 函數中,GetEntity 函數如今須要針對每一種新類型進行修改,咱們須要使用類型斷言來處理返回的值。在實踐中,返回 interface{} 的函數每每很煩人,做爲一個經驗法則,您只須要記住,將 interface{} 做爲參數而不是返interface{} 值一般更好(Postel’s Law)。

咱們也可能會嘗試編寫一些返回類型明確的函數,像這樣:

GetUser(*http.Request) (User, error)
複製代碼

可是這樣又顯得不夠靈活,由於須要對不一樣的類型寫不一樣的函數。咱們真正須要的是像這樣的一個設計:

type Entity interface {
	UnmarshalerHTTP(*http.Request) error
}

func GetEntity(r *http.Request, v Entity) error {
	return v.UnmarshalerHTTP(r)
}
複製代碼

GetEntiry 方法須要傳入一個參數,該參數爲 Entity 接口類型,確保實現了 UnmarshalHTTP 方法。爲了使用該方法,咱們須要定義 User 類型並實現 UnmarshalHTTP 方法,並在方法中解析 HTTP 請求:

type User struct {
   ...
}

func (u *User) UnmarshalHTTP(r *http.Request) error {
   // ...
}
複製代碼

而後,定義一個 User 類型的變量,並將其指針傳遞給 GetEntity 方法:

var u User
if err := GetEntity(req, &u); err != nil {
    // ...
}
複製代碼

這同解析 JSON 數據相似。這種方式能夠始終如一地安全地工做,由於 var u User 將自動地將 User 結構體初始化爲零值。Go 不像其餘語言同樣聲明和初始化是分開進行的。經過聲明一個值而不初始化它,運行時將爲該值分配適當的內存空間。即便咱們的 UnmarshalHTTP 方法不能使用某些字段,這些字段也將包含有效的零數據,而不是垃圾數據。

結語

我但願讀完此文後你能夠更加駕輕就熟地使用 Go 中的接口,記住下面這些結論:

  • 經過考慮數據類型之間的相同功能來建立抽象,而不是相同字段
  • interface{} 的值不是任意類型,而是 interface{} 類型
  • 接口包含兩個字的大小,相似於 (type, value)
  • 函數能夠接受 interface{} 做爲參數,但最好不要返回 interface{}
  • 指針類型能夠調用其所指向的值的方法,反過來不能夠
  • 函數中的參數甚至接受者都是經過值傳遞
  • 一個接口的值就是就是接口而已,跟指針沒什麼關係
  • 若是你想在方法中修改指針所指向的值,使用 * 操做符