[TOC]
## 接口
Go中的接口為指定對象的行為提供了一種方式:如果事情可以*這樣*做,那么它就可以在*這里*使用。我們已經看到一些簡單的例子;自定義的打印可以通過`String`方法來實現,而`Fprintf`可以通過`Write`方法輸出到任意的地方。只有一個或兩個方法的接口在Go代碼中很常見,并且它的名字通常來自這個方法,例如實現`Write`的`io.Writer`。
類型可以實現多個接口。例如,如果一個集合實現了`sort.Interface`,其包含`Len()`,`Less(i, j int) bool`和`Swap(i, j int)`,那么它就可以通過程序包`sort`中的程序來進行排序,同時它還可以有一個自定義的格式器。在這個人造的例子中,`Sequence`同時符合這些條件。
~~~
type Sequence []int
// Methods required by sort.Interface.
func (s Sequence) Len() int {
return len(s)
}
func (s Sequence) Less(i, j int) bool {
return s[i] < s[j]
}
func (s Sequence) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// Method for printing - sorts the elements before printing.
func (s Sequence) String() string {
sort.Sort(s)
str := "["
for i, elem := range s {
if i > 0 {
str += " "
}
str += fmt.Sprint(elem)
}
return str + "]"
}
~~~
## 轉換
`Sequence`的`String`方法重復了`Sprint`對切片所做的工作。如果我們在調用`Sprint`之前,將`Sequence`轉換為普通的`[]int`,則可以共享所做的工作。
~~~
func (s Sequence) String() string {
sort.Sort(s)
return fmt.Sprint([]int(s))
}
~~~
這個對象方法算是轉換技術的另一個例子,從`String`方法中安全地調用`Sprintf`。因為如果我們忽略類型名字,這兩個類型(`Sequence`和`[]int`)是相同的,在它們之間進行轉換是合法的。該轉換并不創建新的值,只不過是暫時使現有的值具有一個新的類型。(有其它的合法轉換,像整數到浮點,是會創建新值的。)
將表達式的類型進行轉換,來訪問不同的方法集合,這在Go程序中是一種常見用法。例如,我們可以使用已有類型`sort.IntSlice`來將整個例子簡化成這樣:
~~~
type Sequence []int
// Method for printing - sorts the elements before printing
func (s Sequence) String() string {
sort.IntSlice(s).Sort()
return fmt.Sprint([]int(s))
}
~~~
現在,`Sequence`沒有實現多個接口(排序和打印),相反的,我們利用了能夠將數據項轉換為多個類型(`Sequence`,`sort.IntSlice`和`[]int`)的能力,每個類型完成工作的一部分。這在實際中不常見,但是卻可以很有效。
## 接口轉換和類型斷言
[類型switch](http://www.hellogcc.org/effective_go.html#type_switch)為一種轉換形式:它們接受一個接口,在switch的每個case中,從某種意義上將其轉換為那種case的類型。這里有一個簡化版本,展示了`fmt.Printf`中的代碼如何使用類型switch將一個值轉換為字符串。如果其已經是字符串,那么我們想要接口持有的實際字符串值,如果其有一個`String`方法,則我們想要調用該方法的結果。
~~~
type Stringer interface {
String() string
}
var value interface{} // Value provided by caller.
switch str := value.(type) {
case string:
return str
case Stringer:
return str.String()
}
~~~
第一種情況找到一個具體的值;第二種將接口轉換為另一個。使用這種方式進行混合類型完全沒有問題。
如果我們只關心一種類型該如何做?如果我們知道值為一個`string`,只是想將它抽取出來該如何做?只有一個case的類型switch是可以的,不過也可以用*類型斷言*。類型斷言接受一個接口值,從中抽取出顯式指定類型的值。其語法借鑒了類型switch子句,不過是使用了顯式的類型,而不是`type`關鍵字:
~~~
value.(typeName)
~~~
結果是一個為靜態類型`typeName`的新值。該類型或者是一個接口所持有的具體類型,或者是可以被轉換的另一個接口類型。要抽取我們已知值中的字符串,可以寫成:
~~~
str := value.(string)
~~~
不過,如果該值不包含一個字符串,則程序會產生一個運行時錯誤。為了避免這樣,可以使用“comma, ok”的習慣用法來安全地測試值是否為一個字符串:
~~~
str, ok := value.(string)
if ok {
fmt.Printf("string value is: %q\n", str)
} else {
fmt.Printf("value is not a string\n")
}
~~~
如果類型斷言失敗,則`str`將依然存在,并且類型為字符串,不過其為零值,一個空字符串。
這里有一個`if`-`else`語句的實例,其效果等價于這章開始的類型switch例子。
~~~
if str, ok := value.(string); ok {
return str
} else if str, ok := value.(Stringer); ok {
return str.String()
}
~~~
## 概述
如果一個類型只是用來實現接口,并且除了該接口以外沒有其它被導出的方法,那就不需要導出這個類型。只導出接口,清楚地表明了其重要的是行為,而不是實現,并且其它具有不同屬性的實現可以反映原始類型的行為。這也避免了對每個公共方法實例進行重復的文檔介紹。
這種情況下,構造器應該返回一個接口值,而不是所實現的類型。作為例子,在hash庫里,`crc32.NewIEEE`和`adler32.New`都是返回了接口類型`hash.Hash32`。在Go程序中,用CRC-32算法來替換Adler-32,只需要修改構造器調用;其余代碼都不受影響。
類似的方式可以使得在不同`crypto`程序包中的流密碼算法,可以與鏈在一起的塊密碼分離開。`crypto/cipher`程序包中的`Block`接口,指定了塊密碼的行為,即提供對單個數據塊的加密。然后,根據`bufio`程序包類推,實現該接口的加密包可以用于構建由`Stream`接口表示的流密碼,而無需知道塊加密的細節。
`crypto/cipher`接口看起來是這樣的:
~~~
type Block interface {
BlockSize() int
Encrypt(src, dst []byte)
Decrypt(src, dst []byte)
}
type Stream interface {
XORKeyStream(dst, src []byte)
}
~~~
這里有一個計數器模式(CTR)流的定義,其將塊密碼轉換為流密碼;注意塊密碼的細節被抽象掉了:
~~~
// NewCTR returns a Stream that encrypts/decrypts using the given Block in
// counter mode. The length of iv must be the same as the Block's block size.
func NewCTR(block Block, iv []byte) Stream
~~~
`NewCTR`并不只是用于一個特定的加密算法和數據源,而是用于任何對`Block`接口的實現和任何`Stream`。因為它們返回接口值,所以將CTR加密替換為其它加密模式只是一個局部的改變。構造器調用必須被修改,不過因為上下文代碼必須將結果只作為`Stream`來處理,所以其不會注意到差別。
## 接口和方法
由于幾乎任何事物都可以附加上方法,所以幾乎任何事物都能夠滿足接口的要求。一個示例是在`http`程序包中,其定義了`Handler`接口。任何實現了`Handler`的對象都可以為HTTP請求提供服務。
~~~
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
~~~
`ResponseWriter`本身是一個接口,提供了對用于向客戶端返回響應的方法的訪問。這些方法包括了標準的`Write`方法,所以任何可以使用`io.Writer`的地方,都可以使用`http.ResponseWriter`。
簡單起見,讓我們忽略POST,假設HTTP請求總是GET;這種簡化不影響建立處理的方式。這里有一個簡單而完整的handler實現,用于計算頁面的訪問次數。
~~~
// Simple counter server.
type Counter struct {
n int
}
func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ctr.n++
fmt.Fprintf(w, "counter = %d\n", ctr.n)
}
~~~
(題外話,注意`Fprintf`是如何能夠打印到`http.ResponseWriter`的。)作為參考,下面給出了如何將該服務附加到URL樹上的節點。
~~~
import "net/http"
...
ctr := new(Counter)
http.Handle("/counter", ctr)
~~~
但是為什么`Counter`為一個結構體?只需要一個整數就可以了。(接收者需要為一個指針,這樣增量才能對調用者可見。)
~~~
// Simpler counter server.
type Counter int
func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
*ctr++
fmt.Fprintf(w, "counter = %d\n", *ctr)
}
~~~
如果你的程序具有某個內部狀態,當頁面被訪問時需要被告知,那么該如何?可以將一個channel綁定到網頁上。
~~~
// A channel that sends a notification on each visit.
// (Probably want the channel to be buffered.)
type Chan chan *http.Request
func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ch <- req
fmt.Fprint(w, "notification sent")
}
~~~
最后,比方說我們想在`/args`上展現我們喚起服務二進制時所使用的參數。這很容易編寫一個函數來打印參數。
~~~
func ArgServer() {
fmt.Println(os.Args)
}
~~~
我們怎么將它轉換成HTTP服務?我們可以將`ArgServer`創建為某個類型的方法,忽略該類型的值,不過有一種更干凈的方式。既然我們可以為除了指針和接口以外的任何類型來定義方法,那么我們可以為函數編寫一個方法。`http`程序包包含了這樣的代碼:
~~~
// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler object that calls f.
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(c, req).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
f(w, req)
}
~~~
`HandlerFunc`為一個類型,其具有一個方法,`ServeHTTP`,所以該類型值可以為HTTP請求提供服務。看下該方法的實現:接收者為一個函數,`f`,并且該方法調用了`f`。這看起來可能有些怪異,但是這與接收者為channel,方法在channel上進行發送數據并無差別。
要將`ArgServer`放到HTTP服務中,我們首先將其簽名修改正確。
~~~
// Argument server.
func ArgServer(w http.ResponseWriter, req *http.Request) {
fmt.Fprintln(w, os.Args)
}
~~~
`ArgServer`現在具有和`HandlerFunc`相同的簽名,所以其可以被轉換為那個類型,然后訪問它的方法,就像我們將`Sequence`轉換為`IntSlice`,來訪問`IntSlice.Sort`一樣。代碼實現很簡潔:
~~~
http.Handle("/args", http.HandlerFunc(ArgServer))
~~~
當有人訪問頁面`/args`時,在該頁上安裝的處理者就具有值`ArgServer`和類型`HandlerFunc`。HTTP服務將會調用該類型的方法`ServeHTTP`,將`ArgServer`作為接收者,其將轉而調用`ArgServer`(通過在`HandlerFunc.ServeHTTP`內部調用`f(c, req)`)。然后,參數就被顯示出來了。
在這章節,我們分別通過結構體,整數,channel,以及函數創建了HTTP服務,這都是因為接口就是一個方法的集合,其可以針對(幾乎)任何類型來定義。