Go 語言中的接口暴露
【導讀】golang 編程中,接口怎麼設計、怎麼寫?接口設計方面有什麼社區推薦的方法和思路?本文作者結合實踐經驗做了詳細介紹。
接口是 Go 語言裏我最喜歡的特性。一個接口類型代表一組方法。與其他大多數語言不同,你不必明確聲明一個類型實現了一個接口。如果一個結構體S
定義了I
所要求的方法,它就隱含地實現了接口I
。
把接口寫得好並不容易。通過暴露廣泛的或不必要的接口,很容易污染包的 "API"。在這篇文章中,我們將解釋現有的接口指南背後的道理,並以標準庫中的例子作爲補充。
接口越大,抽象性越弱
你不太可能找到多個可以實現一個大型接口的類型。由於這個原因,"只有一到兩個方法的接口在 Go 代碼中很常見"。與其聲明大型公共接口,不如考慮使用依賴或是返回一個確定類型。
io.Reader
和io.Writer
接口是強壯接口的比較好的例子:
type Reader interface {
Read(p []byte) (n int, err error)
}
檢索了 std 庫之後,我看到有 30 個包內 81 個結構體實現了io.Reader
,被 99 個方法在 39 個包裏調用到。
Go 接口一般屬於使用接口類型值的包,而不是實現這些值的包
在實際使用接口的包內定義接口,這樣可以讓使用方定義抽象,而不是讓提供接口的一方規定接口的所有抽象。io.Copy
方法就是個例子,它在可以接收Writer
和Reader
接口:
func Copy(dst Writer, src Reader) (written int64, err error)
另一個包裏的例子是color.Color
接口。color.Palette
類型的Index
方法就是可以接收任意實現了Color
接口的結構體:
func (p Palette) Index(c Color) int
如果一個類型的存在只是爲了實現一個接口,並且永遠不會有超出該接口暴露方法,那麼就沒有必要暴露該類型本身
這條原則在代碼 review(https://github.com/golang/go/wiki/CodeReviewComments#interfaces)一文中提及:
實現包應該返回具體的(通常是指針或結構)類型:這樣一來,新的方法可以被添加到實現中,而不需要大量的重構。
配合EffectiveGo
中的說明,我們可以看到在提供方的包內定義一個接口的全貌。
如果一個類型的存在只是爲了實現一個接口,並且永遠不會有超出該接口暴露方法,那麼就沒有必要暴露該類型本身。
rand.Source
接口是一個例子,它會作爲rand.NewSource
方法的返回值使用,返回這個接口。其中的 rngSource 結構體(https://github.com/golang/go/blob/dcd3b2c173b77d93be1c391e3b5f932e0779fb1f/src/math/rand/rng.go#L180)只暴露了Source
和Source64
所需的方法,因此不再需要暴露這個結構體。
返回一個接口而不是返回某個類型對象,這樣做有什麼好處?
返回一個接口的做法能讓你在函數里返回多個實際類型。比如aes.NewSiper
這個構造函數返回了cipher.Block
接口,仔細看這個方法內有兩個實際的類型作爲返回值:
func newCipher(key []byte) (cipher.Block, error) {
...
c := aesCipherAsm{aesCipher{make([]uint32, n), make([]uint32, n)}}
...
if supportsAES && supportsGFMUL {
// Returned type is aesCipherGCM.
return &aesCipherGCM{c}, nil
}
// Returned type is aesCipherAsm.
return &c, nil
}
注意前面的rand
例子裏,接口就是在提供方包rand
內定義的。但是這個例子內接口跑到了另一個ciper
包裏定義。
在我的實踐中,在不同包中定義接口比在同一個包中定義接口要難維護得多。早期開發過程中對調用方的需求會快速迭代,迭代也會體現到定義方包的代碼裏。如果返回值只是個 interface,慢慢地需求可能就會變成需要返回更加明確的類型了。
我假設比較好的實踐是這樣的:
-
返回的接口需要足夠小,這樣就可以返回多個實現的實際類型。
-
在你的包裏有多個類型只實現了這個接口之前,暫不返回 interface。
創建一個單獨的接口專用包,實現命名和標準化。
注意這並不是 Go 語言團隊推薦的做法,這是我的一個觀察結果。在標準庫裏有很多創建一個包只放接口的做法。
hash.Hash
接口就是一個例子,它就是一隻暴露接口的包:https://golang.org/pkg/hash/
package hash
type Hash interface {
...
}
type Hash32 interface {
Hash
Sum32() uint32
}
type Hash64 interface {
Hash
Sum64() uint64
}
我認爲把接口放進一個獨立包有以下兩方面原因:
-
給接口提供一個更好的命名空間。
-
把實現功能這件事標準化。一個只有接口的獨立包暗示着哈希函數應該有
hash.Hash
接口所要求的方法。
另一個例子是encoding
接口:
package encoding
type BinaryMarshaler interface {
MarshalBinary() (data []byte, err error)
}
type BinaryUnmarshaler interface {
UnmarshalBinary(data []byte) error
}
type TextMarshaler interface {
MarshalText() (text []byte, err error)
}
type TextUnmarshaler interface {
UnmarshalText(text []byte) error
}
在 std 庫中有很多實現了encoding
接口的結構體。與被crypto/
下的包使用的 hash 接口不同,std 庫中沒有接受或返回編碼接口的函數。
那麼爲什麼還要暴露這個接口呢?
我認爲這是encoding
包想要給開發者提供一種比較標準化的接口定義方式,希望所有序列化反序列化都如此實現。如果一個新的結構實現了該接口,下面這種寫法判斷是否實現了encoding.BinaryMarshaler
的邏輯都不需要改變其實現:
if m, ok := v.(encoding.BinaryMarshaler); ok {
return m.MarshalBinary()
}
值得注意的是,在compress/zlib
和 compress/flate
軟件包中的Resetter
接口沒有遵循這個模式,因爲它在這兩個軟件包中是重複的。關於這點 Go 語言維護者也有過相關討論(https://codereview.appspot.com/97140043#msg27)。
私有接口不需要考慮前面提到的這些問題,反正這些接口也不會暴露出來
我們可以寫更大的接口而不用擔心其內容,比如encoding/gob
包的gobType
。接口可以在不同的包中重複使用,比如同時存在於os
包和net
包中的timeout
接口,而不用考慮把它們放在一個單獨的位置。
經驗之談
建議最後再寫接口,這時候一般你會對接口有一個比較好的理解和抽象的設計。
對於一個方法提供方,一個好的信號就是多個類型都實現了同一組方法,這時候重構出一個接口就很合適。對於接口的實現方,要保持接口足夠小、足以讓多種結構體都實現這些方法。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/mdWw0YYEWQH_9uoj_dsSEg