🎯 什麼情境該想到我
當你「發現自己在寫一大堆註釋,去解釋一段本身就難懂的程式碼」的時候——註釋在替壞碼擦脂抹粉。
⚙️ 怎麼用(步驟 / 公式)
這味道是什麼徵兆:註釋本身是好東西,但常被當成「除臭劑」——因為碼寫得糟,才需要註釋來解釋。作者的建議:當你想寫註釋時,先試著重構,讓碼好到不需要那條註釋。
通常用哪些重構手法對治:
- 提煉函式(Extract Function):把「需要註釋說明的那段」抽成一個以該註釋為名的函式,讓名字取代註釋。
- 改變函式宣告(Change Function Declaration)/改名:用更誠實的名字,讓意圖寫在名字裡。
- 若註釋在陳述前置條件,考慮**引入斷言(Introduce Assertion)**用程式碼表達。
留下的註釋,應該說「為什麼這樣做」,而不是「這段在做什麼」。
★ 四種註解,只有前兩種是味道
| 例子 | 判斷 | |
|---|---|---|
| ① ★ 在解釋「這段在做什麼」 | // 算未付總額 | ★ 味道。用 提煉函式,把註解內容當函式名 |
| ② ★ 在補救壞名字 | // d 是到期日 | ★ 味道。改名(神秘命名) |
| ③ 在說「為什麼」 | // 這裡不能用 errgroup,因為要保證順序 | ✅ 最有價值的註解,永遠保留 |
| ④ 契約與警告 | // 呼叫端必須持有 mu、// nil 接收者是合法的 | ✅ 不可省略——型別系統表達不了的事 |
★ Fowler 的原話是:註解常常是「除臭劑」(deodorant)——它們的存在是因為程式碼有味道。
但他也明說:「當你不知道該怎麼做時,寫下註解是很好的做法。」
所以這個味道的正確讀法是:「我需要註解來說明它在做什麼」是訊號;「我需要註解來說明為什麼」不是。
★★ Go 特有:這五類註解是必須寫的
// ① ★ 匯出識別字的 doc comment —— 以名字開頭,是 Go 的硬慣例
// Serve 啟動 HTTP 伺服器並阻塞直到 ctx 被取消。
func Serve(ctx context.Context, addr string) error
// ② ★ 併發契約 —— 型別系統完全表達不了,不寫就是埋雷
// Get 是併發安全的。Set 不是;呼叫端必須自行序列化。
// 欄位 items 的讀寫都必須持有 mu。
// ③ ★ nil 接收者/零值可用性
// 對 nil *Logger 呼叫 Log 是合法的,會直接回傳。
// Buffer 的零值是一個可以直接使用的空緩衝區。
// ④ ★ 廢棄標記 —— godoc 與 staticcheck 認得的格式
// Deprecated: 改用 NewClientWithConfig。
func NewClient(addr string) *Client
// ⑤ 編譯器指令(不是註解,是語法)
//go:build linux
//go:generate stringer -type=Kind
//go:embed templates/*★ ②③ 是 Go 最容易被忽略、後果最嚴重的兩類:
介面「只說有這些方法,不說是否併發安全、是否阻塞、複雜度多少」——
那些保證只能寫在註解裡(見 工具-Go介面小而隱式)。
# 相關 linter(golangci-lint,2026-09-24 查證)
# godoclint —— 檢查 godoc 慣例
# godox —— 抓出 TODO / FIXME / HACK
# godot —— 註解要以句號結尾
# dupword —— 註解裡重複的字
# nolintlint —— 檢查 //nolint 指令本身寫得對不對★★ 最該刪的三種註解
// ① ★ 註解掉的程式碼 —— 版本控制就是你的「以備萬一」
// func oldWay() { … }
// 三個傷害:搜尋會命中、不被編譯器檢查所以很快就爛掉、
// 讀者無法判斷它是「暫時關掉」還是「被遺忘」→ 於是沒人敢動它
// ② ★ 已經和程式碼不符的註解 —— 比沒有註解更糟
// 回傳 nil 代表找不到
func Find(id string) (*User, error) { … } // ★ 實作早就改成回 ErrNotFound 了
// ③ 把程式碼唸一遍
i++ // 把 i 加一★ ②「說謊的註解」是最嚴重的:讀者信任它,於是不去讀實作。
→ 所以 doc comment 必須跟著簽章一起改。這也是為什麼 gopls 的 Extract/Inline
會掉註解(Go 的 AST 把註解放在 side table,go.dev/issue/20744)這件事值得警惕——
重構後要回頭檢查註解還在不在、還對不對。
★ 什麼時候這其實不是味道
- ★ 演算法出處與論文引用:
// 參考 Knuth TAOCP vol.2 §4.3.2——無可替代。 - ★ 反直覺的效能/相容性決定:
// 這裡故意不用 strings.Builder,因為 n 幾乎總是 1。 - ★ 工作區的臨時標記:
// TODO(kais): 等 #1234 合併後移除——有負責人、有條件的 TODO 是有用的,
沒有負責人的// TODO才是垃圾。 - 對外 API 的文件:godoc 就是你的 API 文件,寫得越詳細越好。
⚠️ 「自解釋程式碼」被過度引用了
★ 「好的程式碼不需要註解」只對第①②類成立。
「為什麼選 A 不選 B」、「這裡有一個已知的競爭窗口,可接受因為 X」、
「上游 API 在某版本有 bug,所以要重試」——這些資訊不在程式碼裡,也不可能在。
刪掉它們不是整潔,是把團隊知識丟掉。
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
註釋不是壞事:解釋「為什麼」、記錄取捨與陷阱、標示尚不確定之處的註釋都很有價值。
-
別因為這條味道就把所有註釋刪光;先把碼改到自解釋,再保留真正必要的說明。
-
★ 「在解釋做什麼」的註解才是味道;「在解釋為什麼」的註解是最有價值的資產,不要刪。
-
★ Go 的併發契約、nil 接收者可用性、零值可用性,只能寫在註解裡——型別系統表達不了。不寫就是埋雷。
-
★ 說謊的註解比沒有註解更糟。改簽章時必須同步改 doc comment。
-
★ gopls 的 Extract/Inline 會掉註解(go.dev/issue/20744)。重構後回頭檢查。
-
註解掉的程式碼要真的刪。版本控制才是「以備萬一」。
-
無主的
// TODO是垃圾;// TODO(owner): 條件是有用的。用godox定期盤點。 -
匯出識別字的 doc comment 要以名字開頭,這是 Go 的硬慣例,godoc 依賴它。
🔗 相關工具
- 神秘命名 Mysterious Name(好名字能取代大量解釋性註釋)
- 過長函式 Long Function(區塊註釋常是「該提煉函式」的提示)
- 重構
- 提煉函式 —— 「解釋做什麼」的註解,用函式名取代
- 神秘命名 —— 「補救壞名字」的註解,用改名取代
- 移除死程式碼 —— 註解掉的程式碼該真的刪
- 工具-註解是必要之惡 —— 同一主題的即用卡
- 用程式碼溝通、程式碼要清晰表達意圖 —— 讓程式碼自己說話
- 工具-Go介面小而隱式 —— 介面的保證只能寫在註解裡