🎯 什麼情境該想到我

當你「發現自己在寫一大堆註釋,去解釋一段本身就難懂的程式碼」的時候——註釋在替壞碼擦脂抹粉。

⚙️ 怎麼用(步驟 / 公式)

這味道是什麼徵兆:註釋本身是好東西,但常被當成「除臭劑」——因為碼寫得糟,才需要註釋來解釋。作者的建議:當你想寫註釋時,先試著重構,讓碼好到不需要那條註釋。

通常用哪些重構手法對治:

  • 提煉函式(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 依賴它。

🔗 相關工具