🎯 什麼情境該想到我
當你在猶豫「這個參數該收具體型別還是介面」,或「測試時怎麼把資料庫/HTTP 換掉」的時候。
★ 但在 Go 裡,這條原則有一個和 Java 相反的結論——那是本卡的重點。
⚙️ 怎麼用
原則本身
依賴抽象,不依賴具體實作。 呼叫端只認介面,不認是誰實作的。
三個實質好處:
- 可替換(換套件、換演算法、換供應商)
- ★ 可測試(換成測試替身,不碰網路與資料庫)
- 編譯期解耦(改實作不用重編依賴它的套件)
★ 介面該定義在哪一邊(最重要的一個判斷)
❌ Java 式:實作方定義介面,並「宣告我實作它」
package storage
type Storage interface { Get(...); Put(...); List(...); Delete(...); Stat(...) }
type S3 struct{} // implements Storage
package app
func Process(s storage.Storage) // ★ 被迫依賴一個大介面,只用得到 Get
✅ Go 式:★ 消費方定義介面,只放它真正用到的方法
package app
type Getter interface { Get(ctx context.Context, k string) ([]byte, error) } // ★ 只要一個方法
func Process(g Getter) { ... }
package storage
type S3 struct{} // ★ 完全不知道 app.Getter 存在,但天然滿足它
func (S3) Get(...) ([]byte, error)
★ Go 的隱式介面滿足讓「消費方定義介面」成為可能,而這帶來三件事:
- 介面自然會很小(你只寫你用得到的方法)
- 實作方不需要 import 消費方(依賴方向乾淨)
- 別人的型別(第三方套件)也能滿足你的介面,不用改對方程式碼
這就是 Go 諺語 “accept interfaces, return structs” 的由來。
Java 的「介面放在實作旁邊」在 Go 裡是反模式。
★ 「只有一個實作」要不要抽介面
| 情況 | 抽不抽 |
|---|---|
| 只有一個實作,且測試不需要替換它(純計算) | ❌ 不抽。直接用具體型別 |
| 只有一個實作,但測試需要替換(會打網路/資料庫/檔案/時間) | ✅ 抽 ——★ 測試需求本身就是正當理由 |
| 已經有兩個以上實作 | ✅ 抽 |
| 「以後可能會有第二個」 | ❌ 不抽。YAGNI ——真的出現時再抽,那時才知道正確的邊界 |
★ 最常見的過度設計:為每個 struct 配一個同名介面(
UserService+IUserService)。
這在 Go 裡沒有任何好處——它不會讓程式更解耦,只是多一層間接與一個要同步維護的檔案。
實務:Go 的三個標準介面
io.Reader // 一個方法,全世界都能滿足
io.Writer
error // ★ 只有 Error() string★ Go 標準庫最成功的抽象都只有一到兩個方法。 這不是巧合——
介面越小,能滿足它的型別越多,組合的可能性越大。(見 工具-Go介面小而隱式)
TypeScript 的對應
// TS 也是結構型別,同樣「消費方定義」最好
type Getter = { get(key: string): Promise<Uint8Array | null> };
export async function process(g: Getter) { ... }
// ★ 任何有 get 方法的物件都能傳進來,連 class 都不用🧪 我實際套用的紀錄
- 2026-07-14:(待填)
⚠️ 注意 / 什麼時候不適用
- ★ 介面太大 = 沒有介面。一個十個方法的介面,只有原本那一個型別滿足得了——
它沒有提供任何替換性,只是把具體型別換了個名字。 - ★ 不要為了「看起來解耦」而抽。判準是**「有沒有第二個實作,或測試需不需要替換」**。
- 回傳介面通常是錯的(Go):回傳具體型別讓呼叫端拿到完整能力,要不要縮成介面由它決定。
例外:回傳的型別真的有多種(sql.Driver)、或要隱藏內部型別。 - 介面會掩蓋效能特性:
io.Reader背後是記憶體還是網路,呼叫端看不出來。
效能敏感的地方要在文件裡講清楚。 - 動態分派有成本(無法內聯)。極熱的迴圈上,具體型別更快——但先量再說。
- 不要把介面當文件。介面描述「能做什麼」,不描述「保證什麼」(複雜度、是否阻塞、是否並行安全)——那些要寫在註解裡。
🔗 相關工具
- 工具-Go介面小而隱式 —— Go 介面哲學的完整說明
- 工具-優先組合而非繼承 —— 組合的前置條件:被組合的東西要是介面
- 工具-封裝變化點 —— 為什麼要抽:把會變的藏在介面後面
- 工具-開閉原則 —— 目的地
- 工具-打破依賴以便測試 —— 測試替換是抽介面最常見的正當理由
- 策略模式、配接器模式、橋接模式 —— 建立在這條原則上的模式