深入理解Go的interface

NNMEdu 7年前發布 | 15K 次閱讀 Go語言 Google Go/Golang開發

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. 參考

1. Golang "泛型編程"

2. 談一談 Golang 的 interface 和 reflect

3. understanding golang interface(Gopher China) — 油Tube

4. understanding golang interface(Gopher China) — slide

 

來自:http://www.iteye.com/news/32486

 

 本文由用戶 NNMEdu 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!