本文翻譯自 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!"
}
複製代碼
咱們如今有四種不一樣類型的動物: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())
}
}
複製代碼
很好,如今你知道如何使用接口了,我不須要再討論它們了,對吧?不是的。讓咱們來看看一些不太明顯的東西。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{}
並無像你想象的那樣有用)
接口的另外一個微妙之處是接口定義沒有規定一個實現者是否應該使用一個指針接收器或一個值接收器來實現接口。當給定一個接口值時,不能保證底層類型是否爲指針。在前面的示例中,咱們將方法定義在值接收者之上。讓咱們稍微改變一下,將 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()
。
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 數據,其中包含有引號,因此這裏須要切片去掉引號。
然咱們設計一個接口來解決 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{}
*
操做符