防禦性 Golang 程式碼,防止 panic、無聲資料損毀及微妙的執行時期錯誤。當遇到 nil panic、append 別名、map 並行存取、浮點數比較陷阱或零值設計問題時使用。也適用於審查程式碼的 nil 安全性、數值轉換溢位、資源生命週期問題(迴圈中的 defer)或 slice 與 map 的防禦性複製。
角色設定: 你是一位防禦型 Go 工程師。你將每個未經測試的假設(關於 nil、容量和數值範圍)視為潛在的崩潰,隨時可能發生。
Go 安全性:正確性與防禦性程式碼
防止程式設計師錯誤——一般(非對抗性)程式碼中的錯誤、panic 和無聲資料損毀。安全性處理攻擊者;安全性處理我們自己。
最佳實務摘要
- 當型別集合已知時,優先使用泛型而非
any——編譯器會捕捉型別不符,而非執行時期 panic - 永遠使用安全的型別斷言——對於一般介面使用 comma-ok (
v, ok := x.(T));對於 Go 1.25+ 的反射,優先使用reflect.TypeAssert[T](value)而非value.Interface().(T)。 - 介面中的 typed nil 指標不等於
== nil——型別描述器使其非 nil - 寫入 nil map 會 panic——使用前務必初始化
append可能重複使用底層陣列——如果容量允許,兩個 slice 會共享記憶體,無聲地互相破壞- 從匯出函式回傳防禦性複本——否則呼叫者會變更你的內部資料
defer在函式結束時執行,而非迴圈迭代時——將迴圈本體提取到一個函式中- 整數轉換會無聲截斷——
int64轉int32會無警告地繞回 - 浮點數算術不精確——使用 epsilon 比較或
math/big - 設計有用的零值——nil map 欄位在首次寫入時會 panic;使用惰性初始化
- 使用
sync.Once進行惰性初始化——即使在並行下也能保證只執行一次
Nil 安全性
Nil 相關的 panic 是 Go 中最常見的崩潰。
Nil 介面陷阱
介面儲存(型別, 值)。只有當兩者皆為 nil 時,介面才是 nil。回傳 typed nil 指標會設定型別描述器,使其非 nil:
// ✗ 危險——interface{type: *MyHandler, value: nil} 不等於 == nil
func getHandler() http.Handler {
var h *MyHandler // nil 指標
if !enabled {
return h // interface{type: *MyHandler, value: nil} != nil
}
return h
}
// ✓ 良好——明確回傳 nil
func getHandler() http.Handler {
if !enabled {
return nil // interface{type: nil, value: nil} == nil
}
return &MyHandler{}
}
Nil map、slice 和 channel 的行為
| 型別 | 索引 nil | 寫入 nil | Len/Cap of nil | 走訪 nil |
|---|---|---|---|---|
| Map | 零值 | panic | 0 | 0 次迭代 |
| Slice | panic | panic | 0 | 0 次迭代 |
| Channel | 永遠阻塞 | 永遠阻塞 | 0 | 永遠阻塞 |
// ✗ 不好——nil map 在寫入時 panic
var m map[string]int
m["key"] = 1
// ✓ 良好——初始化或在方法中惰性初始化
m := make(map[string]int)
func (r *Registry) Add(name string, val int) {
if r.items == nil { r.items = make(map[string]int) }
r.items[name] = val
}
請參閱 Nil 安全性深入探討 了解 nil 接收器、泛型中的 nil 以及 nil 介面效能。
Slice 與 Map 安全性
Slice 別名——append 陷阱
如果容量允許,append 會重複使用底層陣列。兩個 slice 因此共享記憶體:
// ✗ 危險——a 和 b 共享底層陣列
a := make([]int, 3, 5)
b := append(a, 4)
b[0] = 99 // 也會修改 a[0]
// ✓ 良好——完整 slice 表達式強制新分配
b := append(a[:len(a):len(a)], 4)
Map 並行存取
Map 絕對不能同時存取——→ 請參閱 samber/cc-skills-golang@golang-concurrency 了解同步原語。
請參閱 Slice 和 Map 深入探討 了解走訪陷阱、子 slice 記憶體保留以及 slices.Clone/maps.Clone。
數值安全性
隱含型別轉換會無聲截斷
// ✗ 不好——如果 val > math.MaxInt32,會無聲繞回(30 億變成 -12.9 億)
var val int64 = 3_000_000_000
i32 := int32(val) // -1294967296(無聲繞回)
// ✓ 良好——轉換前先檢查
if val > math.MaxInt32 || val < math.MinInt32 {
return fmt.Errorf("value %d overflows int32", val)
}
i32 := int32(val)
浮點數比較
// ✗ 不好——浮點數算術不精確
var a, b, c float64 = 0.1, 0.2, 0.3
a+b == c // false
// ✓ 良好——使用 epsilon 比較
const epsilon = 1e-9
math.Abs((a+b)-c) < epsilon // true
除以零
整數除以零會 panic。浮點數除以零會產生 +Inf、-Inf 或 NaN。
func avg(total, count int) (int, error) {
if count == 0 {
return 0, errors.New("division by zero")
}
return total / count, nil
}
關於整數溢位作為安全漏洞,請參閱 samber/cc-skills-golang@golang-security 技能章節。
資源安全性
迴圈中的 defer——資源累積
defer 在函式結束時執行,而非迴圈迭代時。資源會累積直到函式回傳:
// ✗ 不好——所有檔案保持開啟直到函式回傳
for _, path := range paths {
f, _ := os.Open(path)
defer f.Close() // 延遲到函式結束
process(f)
}
// ✓ 良好——提取到函式中,讓 defer 每次迭代執行
for _, path := range paths {
if err := processOne(path); err != nil { return err }
}
func processOne(path string) error {
f, err := os.Open(path)
if err != nil { return err }
defer f.Close()
return process(f)
}
Goroutine 洩漏
→ 請參閱 samber/cc-skills-golang@golang-concurrency 了解 goroutine 生命週期和洩漏預防。
不可變性與防禦性複製
匯出函式回傳 slice/map 時應該回傳防禦性複本。
保護結構體內部
// ✗ 不好——匯出的 slice 欄位,任何人都可以變更
type Config struct {
Hosts []string
}
// ✓ 良好——未匯出欄位,搭配回傳複本的存取器
type Config struct {
hosts []string
}
func (c *Config) Hosts() []string {
return slices.Clone(c.hosts)
}
初始化安全性
零值設計
設計型別使 var x MyType 是安全的——防止「忘記初始化」的錯誤:
var mu sync.Mutex // ✓ 零值即可用
var buf bytes.Buffer // ✓ 零值即可用
// ✗ 不好——nil map 在寫入時 panic
type Cache struct { data map[string]any }
sync.Once 用於惰性初始化
type DB struct {
once sync.Once
conn *sql.DB
}
func (db *DB) connection() *sql.DB {
db.once.Do(func() {
db.conn, _ = sql.Open("postgres", connStr)
})
return db.conn
}
init() 函式陷阱
→ 請參閱 samber/cc-skills-golang@golang-design-patterns 了解為何應避免使用 init(),而改用明確的建構子。
使用 Linter 強制執行
許多安全性陷阱可透過 linter 自動捕捉:errcheck、forcetypeassert、nilerr、govet、staticcheck。請參閱 samber/cc-skills-golang@golang-lint 技能了解設定與使用方式。
Go 1.25+ 反射型別斷言
對於反射程式碼,優先使用 reflect.TypeAssert[T] 而非 value.Interface().(T)。
v := reflect.ValueOf(x)
if s, ok := reflect.TypeAssert[string](v); ok {
use(s)
}
交叉參考
- → 請參閱
samber/cc-skills-golang@golang-concurrency技能了解並行存取模式和同步原語 - → 請參閱
samber/cc-skills-golang@golang-data-structures技能了解 slice/map 內部機制、容量增長和 container/ 套件 - → 請參閱
samber/cc-skills-golang@golang-error-handling技能了解 nil 錯誤介面陷阱 - → 請參閱
samber/cc-skills-golang@golang-security技能了解與安全相關的安全性問題(記憶體安全、整數溢位) - → 請參閱
samber/cc-skills-golang@golang-troubleshooting技能了解除錯 panic 和競爭條件
常見錯誤
| 錯誤 | 修正 |
|---|---|
裸型別斷言 v := x.(T) |
型別不符時 panic,導致程式崩潰。使用 v, ok := x.(T) 優雅處理 |
| 在介面函式中回傳 typed nil | 介面持有 (type, nil) 不等於 nil。對 nil 情況回傳未型別的 nil |
| 寫入 nil map | Nil map 沒有底層儲存——寫入會 panic。使用 make(map[K]V) 初始化或惰性初始化 |
假設 append 總是複製 |
如果容量允許,兩個 slice 共享底層陣列。使用 s[:len(s):len(s)] 強制複製 |
迴圈中的 defer |
defer 在函式結束時執行,而非迴圈迭代——資源會累積。將本體提取到獨立函式 |
int64 轉 int32 未做邊界檢查 |
值會無聲繞回(30 億 → -12.9 億)。先檢查 math.MaxInt32/math.MinInt32 |
使用 == 比較浮點數 |
IEEE 754 表示不精確(0.1+0.2 != 0.3)。使用 math.Abs(a-b) < epsilon |
| 整數除法未檢查零 | 整數除以零會 panic。在除法前用 if divisor == 0 保護 |
| 回傳內部 slice/map 參考 | 呼叫者可透過共享底層陣列變更結構體內部。回傳防禦性複本 |
多個 init() 有順序假設 |
init() 跨檔案的執行順序未指定。→ 請參閱 samber/cc-skills-golang@golang-design-patterns——使用明確建構子 |
| 在 nil channel 上永遠阻塞 | Nil channel 在傳送和接收時都會阻塞。使用前務必初始化 |
交叉參考
- → 請參閱
samber/cc-skills-golang@golang-continuous-integration技能了解使用這些指南在 CI 中進行自動化 AI 驅動的程式碼審查






