這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
Understanding Go Interface
0. 引言
在 Golang 中,interface 是一個非常重要的概念和特性,之前寫過兩篇相關的文章:Golang “泛型程式設計”,談一談 Golang 的 interface 和 reflect。然後在 Gopher China 2017 的會上又有一個關於 interface 的 topic: understanding golang interface(Gopher China) — youtube,作者是 Francesc。故在此做一個整理和補充。
1. What is Interface?
In object-oriented programming, a protocol or interface is a common means for unrelated objects) to communicate with each other. These are definitions of methods) and values which the objects agree upon in order to co-operate. — wikipedia
這是 wikipedia 關於 protocal 的定義,將 interface 類比如 protocal 是一種非常助於理解的方式。protocol,中文一般叫做協議,比如網路傳輸中的 TCP 協議。protocol 可以認為是一種雙方為了交流而做出的約定,interface 可以類比如此。
在 Golang 中,interface 是一種抽象類別型,相對於抽象類別型的是具體類型(concrete type):int,string。如下是 io 包裡面的例子。
// Writer is the interface that wraps the basic Write method.//// Write writes len(p) bytes from p to the underlying data stream.// It returns the number of bytes written from p (0 <= n <= len(p))// and any error encountered that caused the write to stop early.// Write must return a non-nil error if it returns n < len(p).// Write must not modify the slice data, even temporarily.//// Implementations must not retain p.type Writer interface { Write(p []byte) (n int, err error)}// Closer is the interface that wraps the basic Close method.//// The behavior of Close after the first call is undefined.// Specific implementations may document their own behavior.type Closer interface { Close() error}
在 Golang 中,interface 是一組 method 的集合,是 duck-type programming 的一種體現。不關心屬性(資料),只關心行為(方法)。具體使用中你可以自訂自己的 struct,並提供特定的 interface 裡面的 method 就可以把它當成 interface 來使用。下面是一種 interface 的典型用法,定義函數的時候參數定義成 interface,調用函數的時候就可以做到非常的靈活。
type MyInterface interface{ Print()}func TestFunc(x MyInterface) {}type MyStruct struct {}func (me *MyStruct) Print() {}func main() { var me Mystruct TestFunc(me)}
2. Why Interface
Gopher China 上給出了下面三個理由:
-
writing generic algorithm (泛型程式設計)
-
hiding implementation detail (隱藏具體實現)
-
providing interception points (不知道如何翻譯)
2.1 writing generic algorithm
嚴格來說,在 Golang 中並不支援泛型程式設計。在 C++ 等進階語言中使用泛型程式設計非常的簡單,所以泛型程式設計一直是 Golang 詬病最多的地方。但是使用 interface 我們可以實現泛型程式設計,我這裡簡單說一下,具體可以參考我前面給出來的那篇文章。比如我們現在要寫一個泛型演算法,形參定義採用 interface 就可以了,以標準庫的 sort 為例。
package sort// A type, typically a collection, that satisfies sort.Interface can be// sorted by the routines in this package. The methods require that the// elements of the collection be enumerated by an integer index.type Interface interface { // Len is the number of elements in the collection. Len() int // Less reports whether the element with // index i should sort before the element with index j. Less(i, j int) bool // Swap swaps the elements with indexes i and j. Swap(i, j int)}...// Sort sorts data.// It makes one call to data.Len to determine n, and O(n*log(n)) calls to// data.Less and data.Swap. The sort is not guaranteed to be stable.func Sort(data Interface) { // Switch to heapsort if depth of 2*ceil(lg(n+1)) is reached. n := data.Len() maxDepth := 0 for i := n; i > 0; i >>= 1 { maxDepth++ } maxDepth *= 2 quickSort(data, 0, n, maxDepth)}
Sort 函數的形參是一個 interface,包含了三個方法:Len()
,Less(i,j int)
,Swap(i, j int)
。使用的時候不管數組的元素類型是什麼類型(int, float, string…),只要我們實現了這三個方法就可以使用 Sort 函數,這樣就實現了“泛型程式設計”。有一點比較麻煩的是,我們需要將數組自訂一下。下面是一個例子。
type Person struct { Name stringAge int}func (p Person) String() string { return fmt.Sprintf("%s: %d", p.Name, p.Age)}// ByAge implements sort.Interface for []Person based on// the Age field.type ByAge []Person //自訂func (a ByAge) Len() int { return len(a) }func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i]}func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }func main() { people := []Person{ {"Bob", 31}, {"John", 42}, {"Michael", 17}, {"Jenny", 26}, } fmt.Println(people) sort.Sort(ByAge(people)) fmt.Println(people)}
另外 Fransesc 在 Gopher China 上還提到了一個比較有趣的東西和大家分享一下。在我們設計函數的時候,下面是一個比較好的準則。
Be conservative in what you send, be liberal in what you accept. — Robustness Principle
對應到 Golang 就是:
Return concrete types, receive interfaces as parameter. — Robustness Principle applied to Go
話說這麼說,但是當我們翻閱 Golang 源碼的時候,有些函數的傳回值也是 interface。
2.2 hiding implement detail
隱藏具體實現,這個很好理解。比如我設計一個函數給你返回一個 interface,那麼你只能通過 interface 裡面的方法來做一些操作,但是內部的具體實現是完全不知道的。Francesc 舉了個 context 的例子。 context 最先由 google 提供,現在已經納入了標準庫,而且在原有 context 的基礎上增加了:cancelCtx,timerCtx,valueCtx。語言的表達有時候略顯蒼白無力,看一下 context 包的代碼吧。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { c := newCancelCtx(parent) propagateCancel(parent, &c) return &c, func() { c.cancel(true, Canceled) }}
表明上 WithCancel 函數返回的還是一個 Context interface,但是這個 interface 的具體實現是 cancelCtx struct。
// newCancelCtx returns an initialized cancelCtx.func newCancelCtx(parent Context) cancelCtx { return cancelCtx{ Context: parent, done: make(chan struct{}), }}// A cancelCtx can be canceled. When canceled, it also cancels any children// that implement canceler.type cancelCtx struct { Context done chan struct{} // closed by the first cancel call. mu sync.Mutex children map[canceler]struct{} // set to nil by the first cancel call err error // set to non-nil by the first cancel call}func (c *cancelCtx) Done() <-chan struct{} { return c.done}func (c *cancelCtx) Err() error { c.mu.Lock() defer c.mu.Unlock() return c.err}func (c *cancelCtx) String() string { return fmt.Sprintf("%v.WithCancel", c.Context)}
儘管內不是實現上下面上個函數返回的具體 struct (都實現了 Context interface)不同,但是對於使用者來說是完全無感知的。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) //返回 cancelCtxfunc WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) //返回 timerCtxfunc WithValue(parent Context, key, val interface{}) Context //返回 valueCtx
2.3 providing interception points
Francesc 這裡的 interception 想表達的意思我理解應該是 wrapper 或者裝飾器,他給出了一個例子如下:
type header struct { rt http.RoundTripper v map[string]string}func (h header) RoundTrip(r *http.Request) *http.Response { for k, v := range h.v { r.Header.Set(k,v) } return h.rt.RoundTrip(r)}
通過 interface,我們可以通過類似這種方式實現 dynamic dispatch。
3. 非侵入式
Francesc 還提到 interface 的非侵入式特性。什麼是侵入式呢?比如 Java 的 interface 實現需要顯示的聲明。
public class MyWriter implements io.Writer {}
這樣就意味著如果要實現多個 interface 需要顯示地寫很多遍,同時 package 的依賴還需要進行管理。Dependency is evil。比如我要實現 io 包裡面的 Reader,Writer,ReadWriter 介面,代碼可以像下面這樣寫。
type MyIO struct {}func (io *MyIO) Read(p []byte) (n int, err error) {...}func (io *MyIO) Write(p []byte) (n int, err error) {...}// io packagetype Reader interface { Read(p []byte) (n int, err error)}type Writer interface { Write(p []byte) (n int, err error)}type ReadWriter interface { Reader Writer}
這種寫法真的很方便,而且不用去顯示的 import io package,interface 底層實現的時候會動態檢測。這樣也會引入一些問題:
-
效能下降。使用 interface 作為函數參數,runtime 的時候會動態確定行為。而使用 struct 作為參數,編譯期間就可以確定了。
-
不知道 struct 實現哪些 interface。這個問題可以使用 guru 工具來解決。
綜上,Golang interface 的這種非侵入實現真的很難說它是好,還是壞。但是可以肯定的一點是,對開發人員來說代碼寫起來更簡單了。
4. interface type assertion
interface 像其他類型轉換的時候一般我們稱作斷言,舉個例子。
func do(v interface{}) { n := v.(int) // might panic}
這樣寫的壞處在於:一旦宣告失敗,程式將會 panic。一種避免 panic 的寫法是使用 type assertion。
func do(v interface{}) { n, ok := v.(int) if !ok { // 宣告失敗處理 }}
對於 interface 的操作可以使用 reflect 包來處理,關於 reflect 包的原理和使用可以參考我的文章。
5. 總結
interface 是 Golang 的一種重要的特性,但是這是以 runtime 為代價的,也就意味著效能的損失(關於 interface 的底層實現之後又時間再寫)。拋開效能不談(現實中使用 Golang 開發的程式 99% 效能都不是問題),interface 對於如何設計我們的代碼確實給了一個很好的思考。
6. 參考
1. [Golang "泛型程式設計"](http://legendtkl.com/2015/11/25/go-generic-programming/)
2. [談一談 Golang 的 interface 和 reflect](http://legendtkl.com/2015/11/28/go-interface-reflect/)
3. [understanding golang interface(Gopher China) — youtube](https://www.youtube.com/watch?v=F4wUrj6pmSI&t=2319s)
4. [understanding golang interface(Gopher China) — slide](https://github.com/gopherchina/conference/blob/master/2017/1.4%20interface.presented.pdf)