📌 30 秒摘要(Layer 3)
貫穿全書的中心思想:簡單、清楚、通用、自動化。從風格、演算法與資料結構、介面設計、除錯、測試、效能到可移植性,作者用最精煉的原則教你寫「經得起時間考驗」的程式。核心信念:與其耍聰明,不如把東西寫到夠簡單,讓正確性顯而易見。
🗺 心智圖(Canvas)
程式設計實務
Link to original 程式設計實務
🧭 簡單・清楚・通用・自動化
✍️ 風格即溝通
🔌 介面設計
🐞 除錯與測試
⚡️ 效能與可移植性
🎯 什麼情境該想到我
當你正想寫一段「很聰明但很難懂」的程式,或方案越滾越複雜時。
⚙️ 怎麼用
先讓它對,再讓它清楚,再讓它快——而且只在必要時才追求快。
- 預設選最簡單、最清楚的寫法;清楚勝過聰明。
- 不要過早最佳化:先量測,確定瓶頸再優化,且只優化熱點。
- 記住:除錯比寫程式更難——寫到極限聰明,就沒餘力除錯它。
- 通用與自動化優先:能少寫特例、能自動化就別手動。
★ 「清楚勝過聰明」的具體對照
// 聰明:一行,用了位元技巧 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 這一層:露出什麼、藏什麼決定了使用者眼中的簡單程度
- 工具-Go可讀性五原則 —— Go 風格指南的具體排序:清楚 > 簡潔 > 簡單 > 可維護 > 一致,以及「最少機制」的升級階梯
清楚的命名、一致的排版、 避免小聰明
🎯 什麼情境該想到我
當你在設計一個模組/函式庫/API 的公開介面,決定「露出什麼、藏什麼」時。
⚙️ 怎麼用(書中第 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 當「找不到資料」的訊號 ★ 「很難用錯」的四個設計手法
- 讓非法狀態無法被建構:
NewOrder(items)在建構時驗證,之後的Order保證合法;而不是讓使用者Order{}再呼叫Validate()。- 用型別取代註解中的約定:
func Sleep(d time.Duration)不會被傳錯單位;func Sleep(ms int)會。- 零值可用:Go 的
sync.Mutex、bytes.Buffer宣告了就能用,不需要初始化——使用者就不可能「忘記初始化」。- 把危險操作做得明顯:
unsafe、Must...、...Unchecked這類名字讓 code review 一眼看到風險。★ 介面設計的時間軸:寧可先小
加一個方法很容易,拿掉一個方法幾乎不可能(會破壞所有使用者)。所以:
- 第一版只放你已經有使用者需要的操作。
- Go 介面尤其如此:對一個已發布的介面加方法,會讓所有外部實作者編譯失敗。常見做法是另定義一個擴充介面(如
io.WriterTo),再用型別斷言探測實作者有沒有支援。- TS 對應:公開型別加必填欄位是破壞性變更;加選填欄位才相容。
🧪 我實際套用的紀錄
- 2026-07-14:(待填)
⚠️ 注意
介面一旦公開就難改;上線前多想一輪,寧可先小再擴。
「正交」不代表「最小化到難用」:
io.ReadAll、os.ReadFile這類便利函式雖然可以由基本操作組出來,但因為幾乎每個使用者都需要,提供它們反而減少錯誤。判準是「使用者自己組會不會常常組錯」。🔗 相關工具
- 工具-管理複雜度、工具-針對介面編程(GoF)、工具-限界上下文(DDD)
- 工具-Go介面小而隱式 —— Go 版本的「小而正交」:介面定義在使用端、只含用到的方法
隱藏實作、資訊隱藏、 簡單且正交的介面
🎯 什麼情境該想到我
當你卡在一個 bug、開始亂改亂試、卻毫無進展時。
⚙️ 怎麼用
- 先讓 bug 可穩定重現:不能重現就先想辦法重現(最小化輸入)。
- 看線索、做假設:讀錯誤訊息與最近的改動,別急著改。
- 二分逼近:用 print/log 或註解,把問題範圍一半一半地縮小。
- 一次只改一個變因,改完驗證,避免製造新 bug。
- 相信但要驗證:你「確定沒問題」的地方,正是最該檢查的地方。
- 修好後補一個測試鎖住它,避免復發。
★ 書中的完整招式(第 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需要看變數在哪一步變壞時 ★ 除錯的科學方法:一次一個假設
- 觀察:錯誤訊息、log、重現步驟。
- 假設:「我認為是 X 造成的」——要具體到可以被證偽。
- 預測:「如果是 X,那改 Y 之後現象應該變成 Z。」
- 實驗:只改 Y,看現象。
- 結論:符合預測 → 假設被支持;不符 → 划掉這個假設,寫下來,換下一個。
最常見的失敗是跳過第 3 步:沒有預測就改,改完現象變了卻不知道代表什麼。
🧪 我實際套用的紀錄
- 2026-07-14:(待填)
⚠️ 注意
難重現的 bug 常來自並發/未初始化/邊界;先穩定環境再查。
「修好了」要證明:bug 消失可能是巧合(時序改變、快取、重啟)。修正前先寫一個會失敗的測試,修正後它變綠——這才證明你修的是對的地方。
書的另一個提醒:除錯時容易「在沒有錯的地方找了很久」——多半是你對程式怎麼運作的心智模型本身就錯了。這時要回到最初的假設重新檢查。
🔗 相關工具
- 對照 工具-區分修復與掩蓋症狀(先找 root cause)、工具-反過來想
好的線索、二分逼近、 讓 bug 可重現、相信但要驗證
先讓它對,再讓它清楚, 再讓它快——而且只在必要時
能自動化就別手動, 減少人為錯誤
🧰 這本書給我的工具
✨ 關鍵重點(Layer 1–2)
- 簡單、清楚、通用、自動化:貫穿全書的四個字。
- 風格即溝通:清楚的命名、一致的排版、避免小聰明。
- 除錯:好的線索、二分逼近、讓 bug 可重現、相信但要驗證。
- 介面:隱藏實作、資訊隱藏、簡單且正交的介面。
- 可移植性與自動化:能自動化就別手動,減少人為錯誤。
💬 金句原文(Layer 0)
- 「除錯的難度是寫程式的兩倍。所以若你把程式寫到極限的聰明,就沒本事除錯它。」
- 「先讓它對,再讓它清楚,再讓它快——而且只在必要時。」