interface 是 Go 語言最精髓的特性之一,一直以來想寫一篇關於 interface 的文章,但是一直沒敢寫。持續幾年之久,還是斗膽總結下。
Concrete types
struct 定義數據的內存佈局。一些早期建議將方法包含在 struct 中,但是被放棄了。相反,方法如普通函數一樣聲明在類型之外。描述 (data) 和行為 (methods) 是獨立且正交的。
一方面,方法只是一個帶有 “receiver” 參數的函數。
type Point struct { x, y float }
func (p Point) Abs() float {
return math.Sqrt(p.x*p.x + p.y*p.y)
}
func Abs(p Point) float {
return math.Sqrt(p.x*p.x + p.y*p.y)
}
Abs 編寫為一個常規函數,功能沒有變化。
什麼時候應該使用方法,什麼時候應該使用函數呢?如果方法不依賴類型的狀態,則應該將其定義為函數。
另一方面,方法在定義其行為時,使用了類型的值時,與所附加的類型緊密關聯。方法可以從對應的類型中獲取值,如果有指針 “receiver”,還可以操縱其狀態。
“類型” 有時候很有用,有時候又很討厭。因為類型是對底層內存佈局的一個抽象,會讓代碼關注於非業務邏輯上的東西,然而代碼又需要在不同類型的數據間做處理。interface 就是其中一種泛型解決方案。
// Package sort provides primitives for sorting slices and user-defined collections.
package sort
// An implementation of Interface can be sorted by the routines in this package.
// The methods refer to elements of the underlying collection by integer index.
type Interface interface {
// Len is the number of elements in the collection.
Len() int
// Less reports whether the element with index i
// must 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.
func Sort(data Interface) {
...
}
Abstract types
Go 的 interface 僅僅是函數的集合,也定義了行為。 interface 與類型之間沒有顯式的關係,類型也可以同時滿足多個 interface 的要求。
type Abser interface {
Abs() float
}
var a Abser
a = Point{3, 4}
print(a.Abs())
a = Vector{1, 2, 3, 4}
print(a.Abs())
Point 和 Vector 滿足 Abser 的要求同時,也符合 interface{} 的要求。不同的是,interface{} 沒有任何行為(method)。
When & How
道理我都懂,但是何時使用,如何使用 interface 呢?
答案是,當不需要關心實現細節的時候?
func fn(Parameter) Result
當函數編寫者希望隱藏實現細節時,應該把 Result 設定為 interface;當函數編寫者希望提供擴展點的時候,應當把 Parameter 設定為 interface;
隱藏實現細節
以 CancelCtx 為例:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}
type cancelCtx struct {
...
}
newCancelCtx 返回值為 cancelCtx。注意到 cancelCtx 是沒有導出的,意味着使用者只能使用 Context 的變量來接收 newCancelCtx 返回值,從而達到隱藏實現的目的。cancelCtx 是否還有其他方法,以及具體如何實現,使用者並無感知。
提供擴展點
當我們需要將文檔持久化
type Document struct {
...
}
// Save writes the contents of the Document to the file f.
func (d *Document) Save(f *os.File) error
假如實現如上,Save 方法將 *os.File 作為寫入的目標。但是此實現存在一些問題:
- 該實現排除了將數據寫入網絡位置的選項。假設網絡存儲成為需求,則此函數的簽名必須更改,從而影響其所有調用者。
- 該實現很難測試。為了驗證其操作,測試必須在寫入文件後讀取文件的內容。還必須確保 f 被寫入到臨時位置,並始終在之後刪除。
- *os.File 暴露了許多與 Save 無關的方法,比如讀取目錄和檢查路徑是否為符號鏈接。
可以使用接口隔離原則重新定義該方法,優化實現為:
// Save writes the contents of d to the supplied ReadWriterCloser.
func (d *Document) Save(rwc io.ReadWriteCloser) error
然而,此方法仍然違反單一職責原則,它同時負責讀取和驗證寫入的內容。將此部分責任拆分走,繼續優化為:
// Save writes the contents of d to the supplied WriteCloser.
func (d *Document) Save(wc io.WriteCloser) error
然而,wc 會在什麼情況下關閉。可能 Save 將無條件調用 Close,或者在成功的情況下調用 Close,以上都不是一個好的選擇。因此再次優化
// WriteTo writes the contents of d to the supplied Writer.
func (d *Document) WriteTo(w io.Writer) error
接口聲明瞭調用方需要的行為,而不是類型將提供的行為。行為的提供方具有高度的擴展空間,例如:裝飾器模式擴展該行為。
type LogWriter struct {
w io.Writer
}
func (l *LogWriter)Write(p []byte) (n int, err error) {
fmt.Printf("write len:%v", len(p))
return l.w.Write(r)
}
總結
關於 interface,很喜歡以下兩句箴言:
Program to an ‘interface’, not an ‘implementation’ —— GoF
Be conservative in what you do, be liberal in what you accept from others —— Robustness Principle
而不是
Return concrete types, receive interfaces as parameter
(由 cancelCtx 的例子可知,如果其類型是導出的 CancelCtx,返回 concrete types 與以上箴言是有出入的)
高級語言賦予了開發者高級的能力,讓開發者不要關注具體值、類型,集中精力去處理業務邏輯(行為,method),interface 提供的就是這種能力。除了 interface,其他問題處理也是基於類似的思路:
Don’t just check errors, handle them gracefully
基於行為處理錯誤,而不是基於值或類型
本文作者:cyningsun
本文地址: https://www.cyningsun.com/08-...
版權聲明:本博客所有文章除特別聲明外,均採用 CC BY-NC-ND 3.0 CN 許可協議。轉載請註明出處!