🎯 什麼情境該想到我

當你在設計一個模組/函式庫/API 的公開介面,決定「露出什麼、藏什麼」時。

⚙️ 怎麼用(書中第 4 章的四原則)

  1. 隱藏實作(資訊隱藏):使用者只看到介面,內部可自由改。
  2. 選一小組正交的基本操作:提供外界需要的全部功能,但絕不更多;功能之間不重疊、可自由組合,同一件事不提供好幾種做法。
  3. 不要在使用者背後搞小動作:不偷寫檔案、不偷改全域變數、改呼叫者的資料要特別謹慎;也不要為了實作者方便,強迫使用者先準備一堆別的東西。
  4. 一致:命名、參數順序、錯誤處理風格全體一致,可預期。

好的介面讓呼叫端「很難用錯」。

同一章小結還有三條配套(資源與錯誤):釋放資源與分配資源應該在同一層次進行、在低層檢查錯誤,在高層處理、只把例外用在異常的情況。

★ 書中的反例:strtok 一次違反兩條

C 的 strtok 是書裡拿來示範「背後搞小動作」的例子:它在輸入字串裡寫入空位元組(偷改呼叫者的資料),又在兩次呼叫之間偷偷保留狀態(用空指標表示「從上次停下的地方繼續」)——結果是意外修改、難以除錯,而且無法同時處理兩個字串,也無法並行使用。更好的設計是一個獨立、無隱藏狀態的函式。

★ 七條原則在 Go 裡長什麼樣

原則Go 的具體做法違反的樣子
隱藏實作不匯出的欄位 + 建構函式;internal/ 目錄由編譯器強制只能被上層目錄的套件匯入匯出 struct 欄位讓人直接改,之後無法加驗證
小而正交io.Reader 只有一個方法,卻能和 bufio、gzip、http 任意組合一個 Store 介面 20 個方法,每個使用者只用其中 2 個
不搞小動作函式不改傳進來的 slice/map,要改就在名字上說清楚(slices.Sort 就地排序,名字和文件都講明)在 init() 裡連 DB、改 http.DefaultClient、偷開 goroutine 而呼叫者無法停止
一致ctx context.Context 永遠是第一個參數、error 永遠是最後一個回傳值、NewX 建構、Close() error 釋放有的函式 (ctx, id)、有的 (id, ctx)
資源同層釋放誰 Open 誰 defer Close();回傳需要關閉的東西時,文件明說「呼叫者必須 Close」(例:resp.Body)函式 A 開檔、函式 B 關檔,錯誤路徑上必漏
低層偵測、高層處理函式庫回傳 error,由呼叫端決定重試、降級或回報函式庫裡 log.Fatal / os.Exit / 自己印 log——使用者無法決定怎麼處理
例外只用在異常panic 只留給程式錯誤;可預期的失敗回傳 error用 panic 當「找不到資料」的訊號

★ 「很難用錯」的四個設計手法

  1. 讓非法狀態無法被建構:NewOrder(items) 在建構時驗證,之後的 Order 保證合法;而不是讓使用者 Order{} 再呼叫 Validate()。
  2. 用型別取代註解中的約定:func Sleep(d time.Duration) 不會被傳錯單位;func Sleep(ms int) 會。
  3. 零值可用:Go 的 sync.Mutex、bytes.Buffer 宣告了就能用,不需要初始化——使用者就不可能「忘記初始化」。
  4. 把危險操作做得明顯:unsafe、Must...、...Unchecked 這類名字讓 code review 一眼看到風險。

★ 介面設計的時間軸:寧可先小

加一個方法很容易,拿掉一個方法幾乎不可能(會破壞所有使用者)。所以:

  • 第一版只放你已經有使用者需要的操作。
  • Go 介面尤其如此:對一個已發布的介面加方法,會讓所有外部實作者編譯失敗。常見做法是另定義一個擴充介面(如 io.WriterTo),再用型別斷言探測實作者有沒有支援。
  • TS 對應:公開型別加必填欄位是破壞性變更;加選填欄位才相容。

🧪 我實際套用的紀錄

  • 2026-07-14:(待填)

⚠️ 注意

  • 介面一旦公開就難改;上線前多想一輪,寧可先小再擴。

  • 「正交」不代表「最小化到難用」:io.ReadAll、os.ReadFile 這類便利函式雖然可以由基本操作組出來,但因為幾乎每個使用者都需要,提供它們反而減少錯誤。判準是「使用者自己組會不會常常組錯」。

🔗 相關工具