golang-safety

golang-safety

熱門

防禦性 Golang 程式碼,防止 panic、無聲資料損毀及微妙的執行時期錯誤。當遇到 nil panic、append 別名、map 並行存取、浮點數比較陷阱或零值設計問題時使用。也適用於審查程式碼的 nil 安全性、數值轉換溢位、資源生命週期問題(迴圈中的 defer)或 slice 與 map 的防禦性複製。

2261星標
150分支
更新於 2026/6/6
SKILL.md
readonlyread-only
name
golang-safety
description

防禦性 Golang 程式碼,防止 panic、無聲資料損毀及微妙的執行時期錯誤。當遇到 nil panic、append 別名、map 並行存取、浮點數比較陷阱或零值設計問題時使用。也適用於審查程式碼的 nil 安全性、數值轉換溢位、資源生命週期問題(迴圈中的 defer)或 slice 與 map 的防禦性複製。

角色設定: 你是一位防禦型 Go 工程師。你將每個未經測試的假設(關於 nil、容量和數值範圍)視為潛在的崩潰,隨時可能發生。

Go 安全性:正確性與防禦性程式碼

防止程式設計師錯誤——一般(非對抗性)程式碼中的錯誤、panic 和無聲資料損毀。安全性處理攻擊者;安全性處理我們自己。

最佳實務摘要

  1. 當型別集合已知時,優先使用泛型而非 any——編譯器會捕捉型別不符,而非執行時期 panic
  2. 永遠使用安全的型別斷言——對於一般介面使用 comma-ok (v, ok := x.(T));對於 Go 1.25+ 的反射,優先使用 reflect.TypeAssert[T](value) 而非 value.Interface().(T)
  3. 介面中的 typed nil 指標不等於 == nil——型別描述器使其非 nil
  4. 寫入 nil map 會 panic——使用前務必初始化
  5. append 可能重複使用底層陣列——如果容量允許,兩個 slice 會共享記憶體,無聲地互相破壞
  6. 從匯出函式回傳防禦性複本——否則呼叫者會變更你的內部資料
  7. defer 在函式結束時執行,而非迴圈迭代時——將迴圈本體提取到一個函式中
  8. 整數轉換會無聲截斷——int64int32 會無警告地繞回
  9. 浮點數算術不精確——使用 epsilon 比較或 math/big
  10. 設計有用的零值——nil map 欄位在首次寫入時會 panic;使用惰性初始化
  11. 使用 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-InfNaN

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 自動捕捉:errcheckforcetypeassertnilerrgovetstaticcheck。請參閱 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 在函式結束時執行,而非迴圈迭代——資源會累積。將本體提取到獨立函式
int64int32 未做邊界檢查 值會無聲繞回(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 驅動的程式碼審查