鍍金池/ 教程/ GO/ 接口
版權(quán)
簡介
Bibliography
接口
通訊
函數(shù)
進階
索引
并發(fā)

接口

在 Go 中,關鍵字 interface 被賦予了多種不同的含義。每個類型都有接口,意味著對那個類型定義了方法集合。這段代碼定義了具有一個字段和兩個方法的結(jié)構(gòu)類型 S。

http://wiki.jikexueyuan.com/project/learn-go-language/images/143.png" alt="pic" />

也可以定義接口類型,僅僅是方法的集合。這里定義了一個有兩個方法的接口 I:

http://wiki.jikexueyuan.com/project/learn-go-language/images/144.png" alt="pic" />

對于接口 I,S 是合法的實現(xiàn),因為它定義了 I 所需的兩個方法。注意,即便是沒有明確定義 S 實現(xiàn)了 I,這也是正確的。

Go 程序可以利用這個特點來實現(xiàn)接口的另一個含義,就是接口值:

http://wiki.jikexueyuan.com/project/learn-go-language/images/145.png" alt="pic" />

0 .定義一個函數(shù)接受一個接口類型作為參數(shù);

  1. p 實現(xiàn)了接口I,必須有 Get() 方法;
  2. Put() 方法是類似的。

這里的變量 p 保存了接口類型的值。因為 S 實現(xiàn)了 I,可以調(diào)用 f 向其傳遞 S 類型的值的指針:

var s S ; f(&s)

獲取 s 的地址,而不是 S 的值的原因,是因為在 s 的指針上定義了方法,參閱上面的代碼 5.1。這并不是必須的——可以定義讓方法接受值——但是這樣的話 Put 方法就不會像期望的那樣工作了。

實際上,無須明確一個類型是否實現(xiàn)了一個接口意味著 Go 實現(xiàn)了叫做 duck typing[26] 的模式。這不是純粹的 duck typing,因為如果可能的話 Go 編譯器將對類型是否實現(xiàn)了接口進行實現(xiàn)靜態(tài)檢查。然而,Go 確實有純粹動態(tài)的方面,如可將一個接口類型轉(zhuǎn)換到另一個。通常情況下,轉(zhuǎn)換的檢查是在運行時進行的。如果是非法轉(zhuǎn)換——當在已有接口值中存儲的類型值不匹配將要轉(zhuǎn)換到的接口——程序會拋出運行時錯誤。

在 Go 中的接口有著與許多其他編程語言類似的思路:C++ 中的純抽象虛基類,Haskell 中的 typeclasses 或者 Python 中的 duck typing。然而沒有其他任何一個語言聯(lián)合了接口值、靜態(tài)類型檢查、運行時動態(tài)轉(zhuǎn)換,以及無須明確定義類型適配一個接口。這些給 Go 帶來的結(jié)果是,強大、靈活、高效和容易編寫的。

到底是什么?

來定義另外一個類型同樣實現(xiàn)了接口 I:

type R s t r u c t { i i n t }
func (p *R) Get() i n t { return p.i }
func (p *R) Put(v i n t ) { p.i = v }

函數(shù) f 現(xiàn)在可以接受類型為 R 或 S 的變量。假設需要在函數(shù) f 中知道實際的類型。在 Go 中可以使用 type switch 得到。

http://wiki.jikexueyuan.com/project/learn-go-language/images/146.png" alt="pic" />

0 .類型判斷。在 switch 語句中使用 (type)。保存類型到變量 t;

  1. p 的實際類型是 S 的指針;
  2. p 的實際類型是 R 的指針;
  3. p 的實際類型是 S;
  4. p 的實際類型是 R;
  5. 實現(xiàn)了 I 的其他類型。

在 switch 之外使用 (type) 是非法的。類型判斷不是唯一的運行時得到類型的方法。為了在運行時得到類型,同樣可以使用 “comma, ok” 來判斷一個接口類型是否實現(xiàn)了某個特定接口:

http://wiki.jikexueyuan.com/project/learn-go-language/images/147.png" alt="pic" />

確定一個變量實現(xiàn)了某個接口,可以使用:

t := something.(I)

空接口

由于每個類型都能匹配到空接口:interface{}。我們可以創(chuàng)建一個接受空接口作為參數(shù)的普通函數(shù):

http://wiki.jikexueyuan.com/project/learn-go-language/images/148.png" alt="pic" />

在這個函數(shù)中的 return something.(I).Get() 是有一點竅門的。值 something 具有類型 interface{},這意味著方法沒有任何約束:它能包含任何類型。.(I) 是類型斷言,用于轉(zhuǎn)換 something 到 I 類型的接口。如果有這個類型,則可以調(diào)用 Get() 函數(shù)。因此,如果創(chuàng)建一個 S 類型的新變量,也可以調(diào)用 g(),因為 S 同樣實現(xiàn)了空接口。

s = new(S)
fmt.Println(g(s)) ;

調(diào)用 g 的運行不會出問題,并且將打印 0。如果調(diào)用 g() 的參數(shù)沒有實現(xiàn) I 會帶來一個麻煩:

http://wiki.jikexueyuan.com/project/learn-go-language/images/149.png" alt="pic" />

這能編譯,但是當運行的時候會得到:

panic: interface conversion: int is not main.I: missing method Get

這是絕對沒問題,內(nèi)建類型 int 沒有 Get() 方法。

方法

方法就是有接收者的函數(shù)(參閱第 2 章)。

可以在任意類型上定義方法(除了非本地類型,包括內(nèi)建類型:int 類型不能有方法)。然而可以新建一個擁有方法的整數(shù)類型。例如:

http://wiki.jikexueyuan.com/project/learn-go-language/images/150.png" alt="pic" />

對那些非本地(定義在其他包的)類型也一樣:

http://wiki.jikexueyuan.com/project/learn-go-language/images/151.png" alt="pic" />

接口類型的方法

接口定義為一個方法的集合。方法包含實際的代碼。換句話說,一個接口就是定義,而方法就是實現(xiàn)。因此,接收者不能定義為接口類型,這樣做的話會引起 invalid receiver type ... 的編譯器錯誤。來自語言說明書 [10] 的權(quán)威內(nèi)容:

接收者類型必須是 T 或 *T,這里的 T 是類型名。T 叫做接收者基礎類型或簡稱基礎類型。基礎類型一定不能使指針或接口類型,并且定義在與方法相同的包中。

Pointers to interfaces

在 Go 中創(chuàng)建指向接口的指針是無意義的。實際上創(chuàng)建接口值的指針也是非法的。在 2010-10-13 的發(fā)布日志中進行的描述,使得沒有任何余地懷疑這一事實:

語言的改變是使用指針指向接口值不再自動反引用指針。指向接口值的指針通常是低級的錯誤,而不是正確的代碼。

這來自[9]。如果不是這個限制,這個代碼:

var buf bytes.Buffer
io.Copy(buf, os.Stdin)

就會復制標準輸入到 buf 的副本,而不是 buf 本身。這看起來永遠不會是一個期望的結(jié)果。

接口名字

根據(jù)規(guī)則,單方法接口命名為方法名加上-er 后綴:Reader,Writer,F(xiàn)ormatter 等。

有一堆這樣的命名,高效的反映了它們職責和包含的函數(shù)名。Read,Write,Close,F(xiàn)lush,String 等等有著規(guī)范的聲明和含義。為了避免混淆,除非有類似的聲明和含義,否則不要讓方法與這些重名。相反的,如果類型實現(xiàn)了與眾所周知的類型相同的方法,那么就用相同的名字和聲明;將字符串轉(zhuǎn)換方法命名為 String 而不是 ToString。

簡短的例子

回顧那個冒泡排序的練習 Q13),對整型數(shù)組排序:

http://wiki.jikexueyuan.com/project/learn-go-language/images/152.png" alt="pic" />

排序字符串的版本是類似的,除了函數(shù)的聲明:

func bubblesortString(n [] s t r i n g ) { /* ... */ }

基于此,可能會需要兩個函數(shù),每個類型一個。而通過使用接口可以讓這個變得更加通用。

來創(chuàng)建一個可以對字符串和整數(shù)進行排序的函數(shù),這個例子的某些行是無法運行的:

http://wiki.jikexueyuan.com/project/learn-go-language/images/153.png" alt="pic" />

  1. 函數(shù)將接收一個空接口的 slice;
  2. 使用 type switch 找到輸入?yún)?shù)實際的類型;
  3. 然后排序;
  4. 返回排序的 slice。

但是如果用 sort([]int{1, 4, 5}) 調(diào)用這個函數(shù),會失?。篶annot use i (type []int) as type []interface in function argument

這是因為 Go 不能簡單的將其轉(zhuǎn)換為接口的 slice。轉(zhuǎn)換到接口是容易的,但是轉(zhuǎn)換到 slice 的開銷就高了。

簡單來說 :Go 不能(隱式)轉(zhuǎn)換為 slice。

那么如何創(chuàng)建 Go 形式的這些“通用” 函數(shù)呢?用 Go 隱式的處理來代替 type switch 方式的類型推斷吧。下面的步驟是必須的:

  1. 定義一個有著若干排序相關的方法的接口類型(這里叫做 Sorter)。至少需要獲取 slice 長度的函數(shù),比較兩個值的函數(shù)和交換函數(shù);

http://wiki.jikexueyuan.com/project/learn-go-language/images/154.png" alt="pic" />

2 .定義用于排序 slice 的新類型。注意定義的是 slice 類型;

type Xi [] i n t
type Xs [] s t r i n g

3 .實現(xiàn) Sorter 接口的方法。整數(shù)的:

http://wiki.jikexueyuan.com/project/learn-go-language/images/155.png" alt="pic" />

和字符串的:

http://wiki.jikexueyuan.com/project/learn-go-language/images/156.png" alt="pic" />

4 .編寫作用于 Sorter 接口的通用排序函數(shù)。

http://wiki.jikexueyuan.com/project/learn-go-language/images/157.png" alt="pic" />

0 .x 現(xiàn)在是 Sorter 類型;

  1. 使用定義的函數(shù),實現(xiàn)了冒泡排序。

現(xiàn)在可以像下面這樣使用通用的 Sort 函數(shù):

http://wiki.jikexueyuan.com/project/learn-go-language/images/158.png" alt="pic" />

在接口中列出接口

看一下下面的接口定義,這個是來自包 container/heap 的:

http://wiki.jikexueyuan.com/project/learn-go-language/images/159.png" alt="pic" />

這里有另外一個接口在 heap.Interface 的定義中被列出,這看起來有些古怪,但是這的確是正確的,要記得接口只是一些方法的列表。sort.Interface 同樣是這樣一個列表,因此將其包含在接口內(nèi)是毫無錯誤的。

自省和反射

在下面的例子中,了解一下定義在 Person 的定義中的“標簽”(這里命名為 “namestr”)。為了做到這個,需要 reflect 包(在 Go 中沒有其他方法)。要記得,查看標簽意味著返回類型的定義。因此使用 reflect 包來指出變量的類型,然后訪問標簽。

http://wiki.jikexueyuan.com/project/learn-go-language/images/160.png" alt="pic" />

0 .We are dealing with a Type and according to the documentationa:

// Elem returns a type’s element type.
// It panics if the type’s Kind is not Array, Chan, Map, Ptr, or Slice.
Elem() Type

同樣的在 t 使用 Elem() 得到了指針指向的值。

  1. 現(xiàn)在已經(jīng)定義了可以“深入”結(jié)構(gòu)體的指針。就可以使用 Field(0) 訪問零值字段;
  2. 結(jié)構(gòu) StructField 有成員 Tag,返回字符串類型的標簽名。因此,在第 0th 個字段上可以用 .Tag 訪問這個名字:Field(0).Tag。這樣得到了 namestr。

為了讓類型和值之間的區(qū)別更加清晰,看下面的代碼:

http://wiki.jikexueyuan.com/project/learn-go-language/images/161.png" alt="pic" />

0 .這里希望獲得“標簽”。因此需要 Elem() 重定向至其上,訪問第一個字段來獲取標簽。注意將 t 作為一個 reflect.Type 來操作;

  1. 現(xiàn)在需要訪問其中一個成員的值,并讓 v 上的 Elem() 進行重定向。這樣就訪問到了結(jié)構(gòu)。然后訪問第一個字段Field (0) 并且調(diào)用其上的 String() 方法。

Figure 5.1. 用反射去除層次關系。通過 Elem() 訪問 *Person,使用 go doc reflect 中描述的方法獲得 string 內(nèi)部包含的內(nèi)容。

http://wiki.jikexueyuan.com/project/learn-go-language/images/162.png" alt="pic" />

設置值與獲得值類似,但是僅僅工作在可導出的成員上。這些代碼:

http://wiki.jikexueyuan.com/project/learn-go-language/images/163.png" alt="pic" />

左邊的代碼可以編譯并運行,但是當運行的時候,將得到打印了棧的運行時錯誤:

panic: reflect.Value.SetString using value obtained using unexported
field

右邊的代碼沒有問題,并且設置了成員變量 Name 為 “Albert Einstein”。當然,這僅僅工作于調(diào)用 Set() 時傳遞一個指針參數(shù)。

練習

Q23. (1) 接口和編譯

  1. 在第 72 頁的代碼 5.3 編譯正?!拖裎闹虚_始描述的那樣。但是當運行的時候,會得到運行時錯誤,因此有些東西有錯誤。為什么代碼編譯沒有問題呢?

Q24. (1) 指針和反射

  1. 在第“自省和反射” 節(jié),第 76 頁的最后一段中,有這樣的描述:

右邊的代碼沒有問題,并且設置了成員變量 Name 為 “Albert Einstein”。當然,這僅僅工作于調(diào)用 Set() 時傳遞一個指針參數(shù)。

為什么是這樣的情況?

Q25. (2) 接口和 max()

  1. 在練習 Q12 中創(chuàng)建了工作于一個整形 slice 上的最大函數(shù)?,F(xiàn)在的問題是創(chuàng)建一個顯示最大數(shù)字的程序,同時工作于整數(shù)和浮點數(shù)。雖然在這里會相當困難,不過還是讓程序盡可能的通用吧。

答案

A23. (1) 接口和編譯

  1. 代碼能夠編譯是因為整數(shù)類型實現(xiàn)了空接口,這是在編譯時檢查的。

修復這個正確的途徑是測試這個空接口可以被轉(zhuǎn)換,如果可以,調(diào)用對應的方法。5.2 列出的 Go 代碼中定義了函數(shù) g ——這里重復一下:

func g(any i n t e r f a c e { }) i n t { return any.(I).Get() }

應當修改為:

http://wiki.jikexueyuan.com/project/learn-go-language/images/164.png" alt="pic" />

如果現(xiàn)在調(diào)用 g(),就不會有運行時錯誤了。在 Go 中這種用法被稱作“comma ok”。

A24. (1) 指針和反射

  1. 當調(diào)用一個非指針參數(shù),變量是復制(call-by-value)的。因此,進行魔法般的反射是在副本上。這樣就不能改變原來的值,僅僅改變副本。

A25. (2) 接口和 max()

  1. 下面的程序計算了最大值。它是 Go 能做到的最通用的形式了。

http://wiki.jikexueyuan.com/project/learn-go-language/images/165.png" alt="pic" />

http://wiki.jikexueyuan.com/project/learn-go-language/images/166.png" alt="pic" />

0 .也可以選擇讓這個函數(shù)的返回值為 interface{},但是這也就意味著調(diào)用者不得不總是使用類型斷言來從接口中解析出實際的類型;

  1. 所有類型定義為整數(shù)。然后進行比較;
  2. 參數(shù)是 float32;
  3. 獲得 a 和 b 中的最大值;
  4. 浮點類型也一樣。
上一篇:并發(fā)下一篇:進階