📌 30 秒摘要(Layer 3)
一條核心準則:程式碼應該讓別人理解它所需的時間最短。全書都是把這條落實成小技巧——表面層次(命名、排版、註解)、簡化迴圈與邏輯、縮小變數作用域、一次只做一件事、把複雜想法先用自然語言講清楚再寫成程式碼。比《整潔之道》更聚焦「表面可讀性」。
🗺 心智圖(Canvas)
編寫可讀程式碼的藝術
Link to original 編寫可讀程式碼的藝術
📏 核心定理
🎨 表面層次的改進
🔀 簡化迴圈與邏輯
🧹 重新組織程式碼
🗣 把想法變程式
程式碼的寫法,應讓別人 理解它所需的時間最小化
好名字:具體勝過抽象, 把資訊塞進名字裡
排版:對齊、一致的空行與順序
註解:只寫有價值的, 記錄「為什麼」而非「做什麼」
先處理特例/提早 return, 避免深巢狀
拆解龐大運算式, 用解釋型變數
🎯 什麼情境該想到我
當一個函式/檔案裡變數一堆、你得記住每個在哪被改、心智負擔很重時。
⚙️ 怎麼用
- 讓變數越晚出現、越早消失:用到才宣告,作用域越小越好。
- 減少變數數量:刪掉只用一次的中間暫存、消掉沒在用的變數。
- 偏好不可變(只賦值一次):越少會變的變數,越好推理。
- 避免全域/大範圍可變狀態:把狀態關進最小的範圍。
★ 兩個可以量的數字:跨度與存活時間
《程式碼大全》把「變數讓人多累」拆成兩個可以數的量:
- 跨度(span):同一個變數兩次相鄰引用之間隔了幾行。跨度大 → 讀到第二次時已經忘了它是什麼。
- 存活時間(live time):從第一次引用到最後一次引用共幾行。存活越久 → 越多行程式碼「可能改到它」,推理時要把它一直記在腦裡。
// ✗ total 從第 1 行活到第 40 行,中間 38 行都可能動到它 total := 0 // … 30 行跟 total 無關的準備工作 … for _, it := range items { total += it.Price } // … 5 行 … return total // ✓ 準備工作放前面,total 在要用的時候才出生,存活 3 行 // … 30 行準備工作 … total := 0 for _, it := range items { total += it.Price } return total更好的是讓它連名字都不需要:
return sumPrice(items)——存活時間 0 行的變數,就是不存在的變數。★ Go 讓作用域變小的語法
語法 效果 if err := f(); err != nil { … }err只活在這個if(含else)裡if v, ok := m[k]; ok { … }同上,comma-ok 結果不外洩 switch x := y.(type) { … }型別斷言後的 x只在 switch 裡用 { … }開一個區塊很少用,但測試中想隔開兩段準備工作時有用 把一段邏輯抽成函式 最徹底:函式內的變數外面看不到 ★ 作用域縮小時的頭號陷阱:
:=遮蔽(shadowing)func load() (cfg Config, err error) { if useCache { cfg, err := readCache() // ← := 在 if 區塊裡「新宣告」了 cfg 和 err _ = cfg _ = err } return cfg, err // 永遠是零值和 nil:內層的賦值沒有寫到外層 }
- 內層
:=只要左邊至少有一個新變數就合法,而在新的區塊裡,所有名字都算新的。go vet預設不檢查遮蔽(shadow分析器不在預設集合裡);要抓得在 golangci-lint 的govet設定打開shadow。- 最簡單的預防:內層要寫回外層變數時用
=,不用:=。★ 閉包與迴圈變數(Go 1.22 起已修正)
Go 1.22 以前,
for i := range xs { go func() { use(i) }() }所有 goroutine 共用同一個i,經典 bug。Go 1.22 起每一輪迭代都是新變數(前提:go.mod的go指令 ≥ 1.22)。舊程式碼裡的i := i現在是多餘的,golangci-lint 的copyloopvar會標出來。TS 對照
- 預設
const,要重新賦值才用let,永遠不用var(var是函式作用域,會被提升,作用域比你以為的大)。- 「不可變」要分清:
const arr = []只是變數不能重新指向,arr.push()照樣能改內容;真正唯讀用readonly T[]或as const。🧪 我實際套用的紀錄
- 2026-07-14:(待填)
⚠️ 注意
目標是「同時要記在腦中的變數變少」,不是硬拆到破碎。
不可變在 Go 沒有語法支援:struct 沒有
const。要保護狀態只能靠「不匯出欄位 + 只提供讀取方法」或回傳副本(注意slices.Clone是淺拷貝)。套件層級的可變變數是作用域最大的變數,判準另見 工具-Go全域狀態的判準。
🔗 相關工具
- 工具-有意義的命名 —— 互補手段,作用域縮不了時,好名字至少讓人知道它是誰的
- 工具-一次只做一件事 —— 更根本的做法,把任務拆開後變數自然各歸各的範圍
🎯 什麼情境該想到我
當一段程式把「取資料 + 轉換 + 驗證 + 輸出」全糊在一起,讀起來很費力時。
⚙️ 怎麼用
- 列出這段程式做了哪些「任務」(常常一寫就是好幾件)。
- 讓這些任務依序、分開進行:一個區塊/函式只推進一件任務。
- 必要時把每件任務提煉成獨立函式(見 工具-提煉函式)。
這條是「函式單一職責」在任何一段程式碼層級的應用,連函式內部都適用。
★ 實際操作:把任務列出來,再重排
《編寫可讀程式碼的藝術》的做法很機械——逐行標註這行在推進哪一件任務,然後把同一任務的行排在一起:
// ✗ 四件事交錯:解析、驗證、查價、組回應 func handleQuote(w http.ResponseWriter, r *http.Request) { var req QuoteReq if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "bad json", 400); return } // 解析 price, err := pricing.Lookup(r.Context(), req.SKU) // 查價 if req.Qty <= 0 { http.Error(w, "qty must be > 0", 400); return } // 驗證(放太晚:已經白查一次價) if err != nil { http.Error(w, "lookup failed", 502); return } // 查價 resp := QuoteResp{SKU: req.SKU, Total: price * int64(req.Qty)} // 組回應 if req.Currency != "" && req.Currency != "TWD" { http.Error(w, "unsupported currency", 400); return } // 驗證(更晚) json.NewEncoder(w).Encode(resp) // 組回應 }標完就看得出問題:驗證散在三個地方,其中兩個在做完昂貴的查價之後。重排成四段:
func handleQuote(w http.ResponseWriter, r *http.Request) { req, err := decodeQuote(r) // 1. 解析 if err != nil { writeErr(w, 400, err); return } if err := req.Validate(); err != nil { // 2. 驗證(全部集中、最先做、不碰外部資源) writeErr(w, 400, err); return } price, err := pricing.Lookup(r.Context(), req.SKU) // 3. 做事 if err != nil { writeErr(w, 502, err); return } writeJSON(w, QuoteResp{SKU: req.SKU, Total: price * int64(req.Qty)}) // 4. 輸出 }重排之後順便修掉了一個效能 bug(無效請求不再打外部服務)——這很常見:任務交錯的程式碼,通常也藏著順序錯誤。
★ 常見的「兩件事黏在一起」
黏在一起的 拆開後 計算 + 輸出(邊算邊 fmt.Println)先算出結果值,再一次輸出——計算部分才能被測試 讀取 + 轉換 + 寫入(邊讀邊改邊寫) 讀成資料結構 → 純函式轉換 → 寫出;中間那段零 I/O、最好測 走訪 + 篩選 + 累加(一個巨大 for) 真的影響可讀性才拆;Go 沒有內建 Map/Filter,硬拆成三個迴圈有時反而更長 業務判斷 + 錯誤處理 錯誤用 guard clause 提早 return,主流程靠左 判準:每一段任務能不能用一句話命名?能 → 它可以是一個區塊或函式;某段要用「和」連接兩件事 → 再拆。
🧪 我實際套用的紀錄
- 2026-07-14:(待填)
⚠️ 注意
「一件事」是相對的抽象層次判斷;能被有意義地再分,就還沒到位。
別為了「一件事」把效能拆掉:三次走訪同一個百萬筆的 slice,跟一次走訪做三件事,差的是記憶體頻寬。熱點上的迴圈合併是合理的——但要留一行註解說為什麼(這正是 why 註解,見 工具-註解是必要之惡)。
🔗 相關工具
- 工具-函式短小且單一職責 —— 同一件事的函式層說法,這張講「一段程式碼」,它講「一個函式」的邊界
- 工具-縮小變數作用域 —— 配套手法:把任務拆開後,順手把各段的變數關進各自的範圍,心智負擔才真的降下來
- 工具-提煉函式 —— 拆解的執行工具:辨認出「這段在做另一件事」之後,用它把那段搬出去命名
先用白話把邏輯講一遍, 再照著寫(命名的鏡子)
🧰 這本書給我的工具
- 工具-縮小變數作用域 — 變數太多、追蹤困難時
- 工具-一次只做一件事 — 一段程式塞太多任務時
✨ 關鍵重點(Layer 1–2)
- 核心定理:讓「理解程式碼所需的時間」最小化。
- 表面改善:好名字(具體勝過抽象)、對齊排版、只寫有價值的註解。
- 簡化控制流:先處理特例/提早 return、避免深巢狀、拆解龐大運算式。
- 把想法變程式:先用白話把邏輯講一遍,再照著寫(命名的鏡子)。
💬 金句原文(Layer 0)
- 「程式碼的寫法,應該讓別人理解它所需的時間最小化。」
🔗 相關
- 更全面的整潔紀律見 程式碼整潔之道(命名/函式/註解在此互補)