📌 30 秒摘要(Layer 3)

貫穿全書的中心思想:簡單、清楚、通用、自動化。從風格、演算法與資料結構、介面設計、除錯、測試、效能到可移植性,作者用最精煉的原則教你寫「經得起時間考驗」的程式。核心信念:與其耍聰明,不如把東西寫到夠簡單,讓正確性顯而易見。

🗺 心智圖(Canvas)

程式設計實務

程式設計實務

🧭 簡單・清楚・通用・自動化

✍️ 風格即溝通

🔌 介面設計

🐞 除錯與測試

⚡️ 效能與可移植性

🎯 什麼情境該想到我

當你正想寫一段「很聰明但很難懂」的程式,或方案越滾越複雜時。

⚙️ 怎麼用

先讓它對,再讓它清楚,再讓它快——而且只在必要時才追求快。

  1. 預設選最簡單、最清楚的寫法;清楚勝過聰明。
  2. 不要過早最佳化:先量測,確定瓶頸再優化,且只優化熱點。
  3. 記住:除錯比寫程式更難——寫到極限聰明,就沒餘力除錯它。
  4. 通用與自動化優先:能少寫特例、能自動化就別手動。

★ 「清楚勝過聰明」的具體對照

// 聰明:一行,用了位元技巧
func isPow2(n int) bool { return n > 0 && n&(n-1) == 0 }
 
// 如果團隊裡多數人看不懂 n&(n-1),至少要有名字和一行 why 註解
// isPow2 回報 n 是否為 2 的冪次。n&(n-1) 會清掉最低位的 1,
// 2 的冪次只有一個 1,清完必為 0。

這個例子說明「簡單清楚」不是禁止技巧,而是:技巧必須被命名並解釋,讓讀者不必重新發明它。真正要避免的是無名的聰明——在業務程式碼中間突然出現一段需要五分鐘才能看懂的運算式。

★ 「先對,再清楚,再快」的實際流程

階段做什麼完成標準
1. 對最直接的寫法,先有測試測試通過
2. 清楚命名、拆函式、刪多餘的東西同事不需要你解說就能讀懂
3. 量在真實規模的輸入上量:Go 用 go test -bench + pprof;線上用 profile知道時間花在哪裡
4. 快(只改熱點)只改 profile 裡排前幾名的函式量到的數字真的變好,且還看得懂

第 3 步不能跳:直覺猜的瓶頸大多是錯的。Go 內建的工具讓這一步幾乎零成本——go test -bench=. -cpuprofile=cpu.out 然後 go tool pprof -top cpu.out。

★ Go 語言本身就是這條原則的產物

Go 刻意拿掉很多「聰明」的語法(沒有三元運算子、沒有運算子多載、泛型晚了十幾年才加且刻意受限),代價是有時要多寫幾行,換到的是任何人寫的 Go 讀起來都差不多。在 Go 裡追求「最短的寫法」通常是逆著語言設計走。

★ 「除錯比寫程式難」的推論

這句話的完整推論(Kernighan 的說法)是:如果你寫程式時已經用上了你全部的聰明才智,那你就沒有足夠的聰明去除錯它。實務意義:

  • 寫程式時保留腦力餘裕,給半年後凌晨三點被叫起來的自己。
  • 程式碼被讀的次數遠多於被寫的次數,最佳化的對象應該是讀者。

🧪 我實際套用的紀錄

  • 2026-07-14:(待填)

⚠️ 注意

  • 「簡單」指認知簡單,不是偷懶少做該做的驗證。

  • 簡單 ≠ 容易:「簡單」是指概念少、交纏少;「容易」是指對我來說熟悉、上手快。引入一個大家都熟悉的大型框架可能很「容易」,但一點也不「簡單」——它帶進了你看不見的大量交纏。

  • 簡單的方案常常比較難想出來:第一個想到的往往是複雜的版本;多花時間在設計上,才能把它削到簡單。

🔗 相關工具

清楚的命名、一致的排版、 避免小聰明

🎯 什麼情境該想到我

當你在設計一個模組/函式庫/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 這類便利函式雖然可以由基本操作組出來,但因為幾乎每個使用者都需要,提供它們反而減少錯誤。判準是「使用者自己組會不會常常組錯」。

🔗 相關工具

隱藏實作、資訊隱藏、 簡單且正交的介面

🎯 什麼情境該想到我

當你卡在一個 bug、開始亂改亂試、卻毫無進展時。

⚙️ 怎麼用

  1. 先讓 bug 可穩定重現:不能重現就先想辦法重現(最小化輸入)。
  2. 看線索、做假設:讀錯誤訊息與最近的改動,別急著改。
  3. 二分逼近:用 print/log 或註解,把問題範圍一半一半地縮小。
  4. 一次只改一個變因,改完驗證,避免製造新 bug。
  5. 相信但要驗證:你「確定沒問題」的地方,正是最該檢查的地方。
  6. 修好後補一個測試鎖住它,避免復發。

★ 書中的完整招式(第 5 章「除錯」)

書把 bug 分成兩類,招式不同:

有好線索、錯誤簡單時(5.2):

招式意思
尋找熟悉的模式「我見過這個」常是解答的開始;常見錯誤都有特有的標誌
檢查最近的改動新版本有錯、舊版本沒有 → 新程式碼一定是問題的一部分
不要兩次犯同樣的錯修好一個 bug 後,問「程式裡其他地方是不是也犯了同樣的錯?」
現在就除錯,不要以後書舉「火星探路者」號:發射前測試時出現過重開機,當時在忙別的而忽略,結果到了火星上每天重開一次
取得堆疊追蹤失敗位置的行號、不該出現的參數值(空指標、超大整數、該正卻負)都是線索
動手前先讀抵抗立刻改程式碼的誘惑,先想清楚;休息一下,讓你看到程式碼實際寫的而不是你以為寫的
把程式碼解釋給別人聽書記載某大學計算中心在諮詢台放了一隻絨毛玩具熊,學生得先跟熊解釋,再找真人

沒有線索、錯誤困難時(5.3):

招式意思
讓錯誤可以重現構造出能穩定觸發的輸入,並包成一個指令就能跑
分而治之把輸入縮到最小;對程式碼也做二分——排除一半,看錯誤還在不在
研究失敗的數字特徵書中例子:字元偶爾消失 → 發現都發生在 1024 位元組邊界 → 緩衝區最後一格被空字元覆蓋
印出輸出來縮小範圍在關鍵點印出狀態,看從哪裡開始不對
寫自我檢查的程式碼在程式裡加斷言,讓它自己報告不一致
寫 log 檔事後才能重建發生了什麼
畫圖散佈圖比一列數字更容易看出異常
使用工具diff、版本控制——可以回到已知良好的狀態
保留紀錄記下試過什麼、結果如何,避免重複、避免漏掉

★ 現代工具把這些招式自動化

書中招式Go 的工具做法
檢查最近的改動 + 二分git bisect rungit bisect start BAD GOOD 後 git bisect run go test ./pkg/ -run TestX,自動在 commit 歷史上二分,找出第一個壞掉的 commit
讓錯誤可重現 + 縮小輸入fuzzing(go test -fuzz=FuzzX,Go 1.18+)找到失敗輸入時自動寫進 testdata/fuzz/,之後每次 go test 都會重跑它
取得堆疊追蹤panic 自帶;卡住的程式送 SIGQUIT(Ctrl+\)會印出所有 goroutine 的堆疊死結、goroutine 洩漏最有用
研究數字特徵-race(資料競爭)、-count=100(重複跑,抓不穩定)「偶爾」失敗的 bug 先用 go test -race -count=100 跑一輪
寫自我檢查的程式碼測試裡的不變式檢查;正式環境的 metrics
使用除錯器Delve:dlv test ./pkg -- -test.run TestX需要看變數在哪一步變壞時

★ 除錯的科學方法:一次一個假設

  1. 觀察:錯誤訊息、log、重現步驟。
  2. 假設:「我認為是 X 造成的」——要具體到可以被證偽。
  3. 預測:「如果是 X,那改 Y 之後現象應該變成 Z。」
  4. 實驗:只改 Y,看現象。
  5. 結論:符合預測 → 假設被支持;不符 → 划掉這個假設,寫下來,換下一個。

最常見的失敗是跳過第 3 步:沒有預測就改,改完現象變了卻不知道代表什麼。

🧪 我實際套用的紀錄

  • 2026-07-14:(待填)

⚠️ 注意

  • 難重現的 bug 常來自並發/未初始化/邊界;先穩定環境再查。

  • 「修好了」要證明:bug 消失可能是巧合(時序改變、快取、重啟)。修正前先寫一個會失敗的測試,修正後它變綠——這才證明你修的是對的地方。

  • 書的另一個提醒:除錯時容易「在沒有錯的地方找了很久」——多半是你對程式怎麼運作的心智模型本身就錯了。這時要回到最初的假設重新檢查。

🔗 相關工具

好的線索、二分逼近、 讓 bug 可重現、相信但要驗證

先讓它對,再讓它清楚, 再讓它快——而且只在必要時

能自動化就別手動, 減少人為錯誤

Link to original

🧰 這本書給我的工具

✨ 關鍵重點(Layer 1–2)

  • 簡單、清楚、通用、自動化:貫穿全書的四個字。
  • 風格即溝通:清楚的命名、一致的排版、避免小聰明。
  • 除錯:好的線索、二分逼近、讓 bug 可重現、相信但要驗證。
  • 介面:隱藏實作、資訊隱藏、簡單且正交的介面。
  • 可移植性與自動化:能自動化就別手動,減少人為錯誤。

💬 金句原文(Layer 0)

  • 「除錯的難度是寫程式的兩倍。所以若你把程式寫到極限的聰明,就沒本事除錯它。」
  • 「先讓它對,再讓它清楚,再讓它快——而且只在必要時。」

🔗 相關