2.3 interface:用行為定義依賴
Go 的 interface 描述的是行為,不是繼承關係。你不需要在 concrete type 上宣告「我實作了某個 interface」;只要方法集合符合,Go 就視為實作。
本章目標
學完本章後,你將能夠:
- 理解 implicit interface 的設計精神
- 寫出小而精準的 interface
- 避免把 concrete type 暴露給不需要的呼叫者
- 用 interface 改善測試與依賴邊界
【觀察】interface 只描述需要的行為
interface 的核心規則是:只描述呼叫者需要的行為,不描述實作者的完整身份。假設有一個函式要把訊息寫到某個目的地;它不需要知道目的地是檔案、記憶體 buffer,還是網路連線,只需要知道對方能 Write。
1type Writer interface {
2 Write(p []byte) (n int, err error)
3}
4
5func WriteMessage(w Writer, message string) error {
6 _, err := w.Write([]byte(message))
7 return err
8}這個 interface 很小,只描述一個行為:寫入 bytes。
【判讀】Go interface 是由使用者定義的需求
在 Go 裡,interface 常由「使用者」定義,而不是由「實作者」定義。
這和很多語言不同。你不需要在某個型別上寫:
1// Go 不需要這種宣告
2type File implements Writerimplicit interface 的核心規則是:只要型別有相同方法,就符合 interface,不需要顯式宣告實作關係。
1type MemoryWriter struct {
2 data []byte
3}
4
5func (m *MemoryWriter) Write(p []byte) (int, error) {
6 m.data = append(m.data, p...)
7 return len(p), nil
8}MemoryWriter 沒有提到 Writer,但它已經符合 Writer。
【策略】interface 越小,依賴越清楚
小 interface 的核心規則是:interface 應由使用端需要的最小行為組成。Go 常見的好 interface 很小:
1type Reader interface {
2 Read(p []byte) (n int, err error)
3}
4
5type Writer interface {
6 Write(p []byte) (n int, err error)
7}
8
9type Closer interface {
10 Close() error
11}小 interface 的好處是:
- 呼叫者只依賴自己真正需要的行為
- 測試替身容易寫
- concrete type 可以在不同情境中被重用
- 未來改內部結構時,外部影響較小
反例是把太多方法塞進一個 interface:
1type UserService interface {
2 CreateUser(User) error
3 UpdateUser(User) error
4 DeleteUser(string) error
5 FindUser(string) (User, error)
6 ListUsers() ([]User, error)
7 ExportUsers() ([]byte, error)
8}如果某個函式只需要查詢 user,卻依賴整個 UserService,它就知道太多了。
【執行】為查詢需求設計小介面
依賴邊界的核心規則是:使用端只依賴自己需要的方法。假設一個 HTTP handler 只需要查詢使用者名稱:
1type UserLookup interface {
2 FindName(userID string) (string, bool)
3}
4
5type Handler struct {
6 users UserLookup
7}
8
9func NewHandler(users UserLookup) *Handler {
10 return &Handler{users: users}
11}這個 handler 不知道 user 是存在 map、資料庫、檔案,還是測試假物件裡。它只知道自己需要 FindName。
測試時可以寫一個很小的 fake:
1type fakeUsers map[string]string
2
3func (f fakeUsers) FindName(userID string) (string, bool) {
4 name, ok := f[userID]
5 return name, ok
6}這就是 Go interface 最實用的地方:它讓依賴變小,讓測試變簡單。
何時先保留 concrete type
interface 的使用邊界是:替換需求或測試替身需求清楚時,再抽出小介面。以下情境通常適合先保留 concrete type:
- 只有一個 concrete type,而且沒有測試替身需求
- interface 只是完整複製 concrete type 的所有方法
- 你還不確定呼叫者真正需要哪些行為
Go 的常見做法是:先寫 concrete type,等使用端出現明確需求,再抽小 interface。