🎯 什麼情境該想到我
當一個函式/檔案裡變數一堆、你得記住每個在哪被改、心智負擔很重時。
⚙️ 怎麼用
- 讓變數越晚出現、越早消失:用到才宣告,作用域越小越好。
- 減少變數數量:刪掉只用一次的中間暫存、消掉沒在用的變數。
- 偏好不可變(只賦值一次):越少會變的變數,越好推理。
- 避免全域/大範圍可變狀態:把狀態關進最小的範圍。
★ 兩個可以量的數字:跨度與存活時間
《程式碼大全》把「變數讓人多累」拆成兩個可以數的量:
- 跨度(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全域狀態的判準。
🔗 相關工具
- 工具-有意義的命名 —— 互補手段,作用域縮不了時,好名字至少讓人知道它是誰的
- 工具-一次只做一件事 —— 更根本的做法,把任務拆開後變數自然各歸各的範圍