深入理解Go的interface
0. 引言
在 Golang 中,interface 是一個非常重要的概念和特性,之前寫過兩篇相關的文章:Golang “泛型編程”,談一談 Golang 的 interface 和 reflect。然后在 Gopher China 2017 的會上又有一個關于 interface 的 topic: understanding golang interface(Gopher China) — 油Tube,作者是 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)
}</code></pre>
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(nlog(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 2ceil(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)
}</code></pre>
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)
}</code></pre>
另外 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)
}</code></pre>
盡管內不是實現上下面上個函數返回的具體 struct (都實現了 Context interface)不同,但是對于使用者來說是完全無感知的。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) //返回 cancelCtx
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) //返回 timerCtx
func 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 package
type 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 { // 斷言失敗處理
}
}</code></pre>
對于 interface 的操作可以使用 reflect 包來處理,關于 reflect 包的原理和使用可以參考我的文章。
5. 總結
interface 是 Golang 的一種重要的特性,但是這是以 runtime 為代價的,也就意味著性能的損失(關于 interface 的底層實現之后又時間再寫)。拋開性能不談(現實中使用 Golang 開發的程序 99% 性能都不是問題),interface 對于如何設計我們的代碼確實給了一個很好的思考。
6. 參考
2. 談一談 Golang 的 interface 和 reflect
3. understanding golang interface(Gopher China) — 油Tube
4. understanding golang interface(Gopher China) — slide
來自:http://www.iteye.com/news/32486