🎯 什麼情境該想到我
當你在除錯,或者想讓下一次除錯更容易的時候。
章導言引家具師傅 Jeff Miller:「你也许会对木匠那毫无差错的工作印象深刻。但我向你保证,事实不是这样的。真正的高手只是知道如何亡羊补牢。」書中指出除錯真正的問題是無法預估時間——可能一小時,也可能一週。
| 習慣 | 良策 | 站內的深入版本 |
|---|---|---|
| 記錄問題解決日誌 | 維護一個問題及解決方案的日誌 | — |
| 警告就是錯誤 | 把警告當成錯誤 | 工具-破窗理論與軟體熵 |
| 對問題各個擊破 | 把問題與周圍隔離開 | 工具-系統化除錯 |
| 報告所有的異常 | 處理或向上傳播所有的例外 | 工具-Go錯誤該包還是該轉 |
| 提供有用的錯誤信息 | 展示有用的錯誤訊息,但別讓使用者陷入細節 | 工具-Go現代錯誤處理 |
出處:Subramaniam & Hunt《高效程式設計師的 45 個習慣》第 7 章。書中範例是 C# 與 Java(例外);Go 用回傳
error,對應寫法另加註。引文保留原書簡體字。
⚙️ 怎麼用(逐項)
記錄問題解決日誌
Keep a Solutions Log 什麼時候會冒出來:同一個怪問題又踩了一次/上次是怎麼解的完全忘記了/想把踩過的坑記下來給以後查/團隊裡重複在解一樣的疑難雜症
魔鬼的誘惑:「以前解决过的问题,现在还是可以解决掉的。」
良策:「维护一个问题及其解决方案的日志。保留解决方案是修复问题过程的一部分。」(旁註:不要在同一個地方摔倒兩次)
書中重點:
- 常見的情況:熟悉的問題又出現了,卻忘了上次怎麼修的。上網搜尋也要花時間,而且常常只找到一堆意見而不是解法。
- 日誌可以記錄的內容:問題發生的日期、問題簡述、解決方案的詳細說明、參考的文章或網址、相關的程式碼片段、設定或截圖。
- 用可搜尋的格式存放,用關鍵字查找;放在共享的地方(網路硬碟、Wiki),鼓勵其他人使用與更新。
- 書中的日誌範例:「04/27/2006: If you use KQED version 6 or earlier, you have to rename the base directory to _kqed6 to avoid a conflict with the in-house Core library」——具體、有版本、有解法。
切身感受:「对于某些类似但是有差异的问题,也能从中获得修复的指引。」
平衡的藝術:
- 記錄問題的時間不能超過解決問題的時間;保持輕量,不需要對外發布的品質。
- 用足夠的關鍵字,才找得到以前的解法。
- 搜尋網路發現沒有人遇過同樣的問題——也許是你的搜尋方式有問題。
- 記錄問題發生時的版本:同樣的問題在不同版本可能表現不同。
- 記錄團隊做重大決策的原因:否則 6~9 個月後很難回顧,容易互相指責。
在今天的樣子:
- 團隊層級:runbook、內部知識庫、事故報告的「已知問題」清單。
- 個人層級:這個知識庫本身就是一個問題解決日誌。
- 最後一條「記錄決策原因」,就是架構決策記錄(ADR)。
- 下一次遇到問題時,先查日誌再開始除錯(見 對問題各個擊破 的平衡的藝術)。
警告就是錯誤
Warnings Are Errors 什麼時候會冒出來:編譯輸出一堆警告都直接無視/警告多到看不出哪個是真問題/想把警告直接設成建置失敗/靜態分析的報告從來沒人處理
魔鬼的誘惑:「编译器的警告信息只不过是给过分小心和过于书呆子气的人看的……干脆忽略它们就是了。」
良策:「将警告视为错误。签入带有警告的代码,就跟签入有错误或者没有通过测试的代码一样,都是极差的做法。」
書中重點:
- Venkat 在一個客戶那裡發現開發中的應用程式有 300 多個警告,其中一個被忽略的是:「條件式中的賦值永遠是常數,你是不是要用 == 而不是 =?」對應的程式碼是
if (theTextBox.Visible = true)——條件永遠成立。 - C# 的例子:子類別用
virtual而不是override宣告同名方法,結果透過基底類別參考呼叫時執行的是基底的版本。編譯顯示「建置:1 個成功」,只在輸出視窗裡有一行警告。 - 做法:讓編譯器把警告當錯誤(GCC 的
-Werror、IDE 的專案設定),CI 伺服器也要同樣設定。在專案開始時就設定好,專案中途才改,可能一次冒出幾百個錯誤。 - 「编译器可以轻易处理警告信息,可是你不能。」
平衡的藝術:
- 直譯語言通常也有執行期警告的旗標,要開啟並處理。
- 少數警告因為編譯器或第三方程式碼的問題無法消除——真的沒辦法就不要再浪費時間,但這種情況很少。
- 被棄用的方法有它被棄用的原因,不要再用;至少安排一個迭代安全地移除它們。
- 把自己的方法標為棄用時,要記錄使用者該改用什麼、以及何時移除。
在今天的樣子:
- Go 的編譯器把「未使用的變數」和「未使用的 import」直接當成錯誤——這個習慣被做進了語言本身。
- 其他的檢查交給
go vet和 golangci-lint,在 CI 中設為會讓建置失敗。舊專案可以用--new-from-rev只擋新增的問題(見 工具-破窗理論與軟體熵 的棘輪做法)。 - 棄用:在 Go 的文件註解加上
// Deprecated: 改用 X。,工具會標示出來。
對問題各個擊破
Attack Problems in Isolation 什麼時候會冒出來:在龐大系統裡大海撈針找問題/完全不知道錯出在哪一層/想做出最小可重現的例子/想把可疑的元件抽出來單獨測
魔鬼的誘惑:「要调试一个明显的错误,只有去查看整个系统的代码,而且要全部过一遍。」
良策:「对问题各个击破。在解决问题时,要将问题域与其周边隔离开,特别是在大型应用中。」(旁註:用原型來隔離問題)
書中重點:
- 單元測試迫使程式碼分層;依賴其他模組時用 mock 物件隔離,出問題時比較容易定位。「不要试图马上了解系统的所有细节。」
- Fred 和 George 的故事:在時程緊迫的專案中遇到嚴重的資料損壞。資料庫程式碼沒有和應用程式分開,很難定位,也不能把整個程式碼庫寄給廠商。他們寫了一個小型原型重現症狀,寄給廠商請專家看。就算原型無法重現問題,也等於提供了一個可運作的範例,同樣有助於隔離問題。
- 「你不可能在半空中修理飛機引擎」——要拆下來放到工作台上。
- 程式碼彼此糾纏(「大泥球」)時,最好花時間把相關的程式碼抽出來,建一個可以執行的測試環境。
切身感受:「感觉就像在一个茶杯中寻找一根针,而不是大海捞针。」
平衡的藝術:
- 把程式碼從執行環境中抽出來後,問題消失了——這也幫助你隔離了問題(問題在環境裡)。
- 抽出來後問題還在——同樣幫助你隔離了問題(問題在程式碼裡)。
- 二分搜尋定位問題很有用:把問題空間切一半,看哪一半有問題,再切一半。
- 開始攻擊問題之前,先查問題解決日誌。
在今天的樣子:
- 最小重現案例是回報開源專案 bug 的標準要求——也是 Fred 和 George 的做法。
- 二分搜尋:
git bisect run在 commit 歷史上自動二分(見 工具-系統化除錯)。 - 《程式設計實務》的除錯章節有同樣的「分而治之」招式,兩本書可以對照著看。
報告所有的異常
Report All Exceptions 什麼時候會冒出來:例外接起來卻什麼都沒做/錯誤被默默吞掉查不到原因/不知道該當場處理還是往上拋/系統壞了紀錄裡卻一片乾淨
魔鬼的誘惑:「不要让程序的调用者看到那些奇怪的异常……把你调用的一切都包起来,然后发送自己定义的异常——或者干脆自己解决掉。」
良策:「处理或是向上传播所有的异常。不要将它们压制不管,就算是临时这样做也不行。」
書中的案例:
- Venkat 使用一個很流行的開源函式庫,某個應該建立物件的方法總是回傳 null。下載原始碼後才發現:底層的方法偵測到系統缺少必要的元件,拋出了一個帶有說明的例外——但上層的方法用空的 catch 區塊把例外吞掉,然後回傳 null。他讀了函式庫的程式碼才知道問題,安裝了缺少的元件。
- 一個大型航空訂位系統當機,飛機停飛、上千旅客滯留、航空運輸大亂好幾天——原因是一台應用伺服器上發生了一個未檢查的例外。
切身感受:「当出现问题时,心里知道能够得到抛出的异常。而且没有空的异常处理方法。」
平衡的藝術:
- 決定由誰負責處理例外,是設計工作的一部分。
- 不是所有問題都應該拋出例外。
- 回報的例外要在程式碼的脈絡中有意義——拋出
NullPointerException就像拋出一個 null 物件一樣沒有幫助。 - 有執行期的除錯日誌時,捕捉或拋出例外都要記錄。
- 沒有人願意呼叫一個會拋出 31 種檢查例外的方法——這是設計問題,要解決而不是隨便打補丁。
Go 的對應:
- 「空的 catch 區塊」在 Go 裡是
_ = f()或if err != nil { return nil }——同樣吞掉了原因。golangci-lint 的errcheck會抓出沒有檢查的錯誤。 - 「處理或傳播」在 Go 就是處理或回傳,只做一件:不要又記 log 又回傳(見 工具-Go錯誤該包還是該轉)。
- 傳播時用
%w包裝並加上脈絡,讓上層能用errors.Is判斷(見 工具-Go現代錯誤處理)。
提供有用的錯誤信息
Provide Useful Error Messages 什麼時候會冒出來:錯誤訊息只寫著發生錯誤/使用者回報時說不清楚哪裡壞了/看到例外訊息完全無法定位/想讓錯誤訊息帶上情境跟輸入
魔鬼的誘惑:「要提供给他们干净整洁的错误信息。要使用如下这样让人舒服的词句:‘用户错误。替换,然后继续。’」
良策:「展示有用的错误信息。提供更易于查找错误细节的方式,发生问题时,要展示出尽量多的支持细节,不过别让用户陷入其中。」
書中重點:
- 「操作無法執行」這種訊息比當機好,但對診斷沒有幫助。
- 兩全的做法:顯示一個清楚的說明(「無法登入」)加上一個「詳細資料」的連結,點開可以看到完整的例外鏈(「驗證密碼時出錯 → 連接資料庫時出錯」)。正式環境則提供一個錯誤編號,支援人員請使用者唸出編號,就能在日誌中快速定位。
- 日誌可以記錄當時的系統狀態,方便重建問題——但密碼、銀行帳號這類敏感資訊不應顯示,甚至不應記入日誌。
- 側欄〈區分錯誤類型〉:
- 程式缺陷(真正的 bug):使用者和管理員都無能為力。
- 環境問題(資料庫連不上、磁碟滿了、權限不足):程式設計師沒辦法,但管理員應該能解決。
- 使用者錯誤:告訴使用者哪裡操作有誤,他可以重來。
- 依類型給不同的對象合適的建議。
平衡的藝術:
- 「找不到檔案」沒有幫助;「無法開啟 /andy/project/main.yaml 以供讀取」有用得多。
- 不必等例外才發現問題;在關鍵點用斷言,失敗時提供同樣詳細的資訊。
- 提供更多資訊時,不要洩露安全資訊、個人隱私、商業機密。
- 給使用者的訊息可以包含一個鍵值,方便在日誌中定位。
在今天的樣子:
- 「錯誤編號」就是 request ID/trace ID:回應中帶給使用者,日誌中同樣記錄,支援人員一查就找到(見 工具-遙測與監控)。
- 書中的三種錯誤類型對應到 HTTP 狀態碼:使用者錯誤是 4xx,環境問題與程式缺陷是 5xx——而且 5xx 的回應不應該把內部細節顯示給使用者。
- Go 的錯誤訊息慣例(小寫、無句號、逐層加上脈絡)見 工具-Go錯誤該包還是該轉。
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
- 這一章的價值在「下一次」:日誌、零警告、有意義的錯誤訊息,都是為了讓下一次除錯更快。在趕時程時最容易被省略,也最容易在之後付出代價。
- 錯誤訊息的詳細程度要分對象:開發環境越詳細越好;正式環境對外只給錯誤類型與編號,細節留在日誌。
🔗 相關工具
- 工具-系統化除錯 —— 《程式設計實務》的完整除錯招式,與本頁互補
- 工具-用例外處理錯誤 —— 《程式碼整潔之道》的錯誤處理原則在 Go 與 TS 的對應
- 高效程式設計師的45個習慣 —— 書卡