golang-safety

golang-safety

热门

防御性 Golang 编码,防止恐慌、静默数据损坏和微妙的运行时错误。在遇到 nil 恐慌、append 别名、map 并发访问、浮点数比较陷阱或零值设计问题时使用。也用于审查代码的 nil 安全性、数值转换溢出、资源生命周期问题(循环中的 defer)或切片和映射的防御性复制。

2261Star
150Fork
更新于 2026/6/6
SKILL.md
只读
名称
golang-safety
描述

防御性 Golang 编码,防止恐慌、静默数据损坏和微妙的运行时错误。在遇到 nil 恐慌、append 别名、map 并发访问、浮点数比较陷阱或零值设计问题时使用。也用于审查代码的 nil 安全性、数值转换溢出、资源生命周期问题(循环中的 defer)或切片和映射的防御性复制。

角色: 你是一名防御性 Go 工程师。你将每一个关于 nil、容量和数值范围的未经测试的假设视为潜在的崩溃隐患。

Go 安全性:正确性与防御性编码

防止程序员错误——正常(非对抗性)代码中的错误、恐慌和静默数据损坏。安全性处理攻击者;安全处理我们自己。

最佳实践总结

  1. 当类型集已知时,优先使用泛型而非 any——编译器捕获类型不匹配,而非运行时恐慌
  2. 始终使用安全的类型断言——对于普通接口使用逗号-ok 模式(v, ok := x.(T));对于 Go 1.25+ 的反射,优先使用 reflect.TypeAssert[T](value) 而非 value.Interface().(T)
  3. 接口中的类型化 nil 指针不等于 == nil——类型描述符使其非 nil
  4. 向 nil map 写入会引发恐慌——始终在使用前初始化
  5. append 可能重用底层数组——如果容量允许,两个切片共享内存,静默地相互破坏
  6. 从导出函数返回防御性副本——否则调用者会修改你的内部数据
  7. defer 在函数退出时执行,而非循环迭代——将循环体提取到一个函数中
  8. 整数转换静默截断——int64int32 的转换会无错误地回绕
  9. 浮点数运算不精确——使用 epsilon 比较或 math/big
  10. 设计有用的零值——nil map 字段在首次写入时引发恐慌;使用惰性初始化
  11. 使用 sync.Once 进行惰性初始化——即使在并发下也保证只执行一次

Nil 安全性

与 nil 相关的恐慌是 Go 中最常见的崩溃。

nil 接口陷阱

接口存储(类型,值)。只有当两者都为 nil 时,接口才为 nil。返回类型化的 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、切片和通道的行为

类型 索引 nil 写入 nil 长度/容量 nil 遍历 nil
Map 零值 恐慌 0 0 次迭代
Slice 恐慌 恐慌 0 0 次迭代
Channel 永远阻塞 永远阻塞 0 永远阻塞
// ✗ 坏 — nil map 在写入时引发恐慌
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 安全性深入探讨

切片和映射安全性

切片别名——append 陷阱

如果容量允许,append 会重用底层数组。两个切片随后共享内存:

// ✗ 危险 — a 和 b 共享底层数组
a := make([]int, 3, 5)
b := append(a, 4)
b[0] = 99 // 也会修改 a[0]

// ✓ 好 — 完整切片表达式强制新分配
b := append(a[:len(a):len(a)], 4)

map 并发访问

map 绝不能并发访问——有关同步原语,请参见 samber/cc-skills-golang@golang-concurrency

有关遍历陷阱、子切片内存保留以及 slices.Clone/maps.Clone,请参阅 切片和映射深入探讨

数值安全性

隐式类型转换静默截断

// ✗ 坏 — 如果 val > math.MaxInt32,则静默回绕(3B 变为 -1.29B)
var val int64 = 3_000_000_000
i32 := int32(val) // -1294967296(静默回绕)

// ✓ 好 — 在转换前检查
if val > math.MaxInt32 || val < math.MinInt32 {
    return fmt.Errorf("值 %d 溢出 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

除以零

整数除以零会引发恐慌。浮点数除以零会产生 +Inf-InfNaN

func avg(total, count int) (int, error) {
    if count == 0 {
        return 0, errors.New("除以零")
    }
    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 泄漏

→ 有关 goroutine 生命周期和泄漏预防,请参见 samber/cc-skills-golang@golang-concurrency

不可变性与防御性复制

导出函数返回切片/映射时,应返回防御性副本。

保护结构体内部

// ✗ 坏 — 导出的切片字段,任何人都可以修改
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 在写入时引发恐慌
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() 函数陷阱

→ 有关为什么应避免使用 init() 而使用显式构造函数的详细信息,请参见 samber/cc-skills-golang@golang-design-patterns

使用 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 技能
  • → 有关切片/映射内部、容量增长和 container/ 包,请参见 samber/cc-skills-golang@golang-data-structures 技能
  • → 有关 nil 错误接口陷阱,请参见 samber/cc-skills-golang@golang-error-handling 技能
  • → 有关安全相关的安全问题(内存安全、整数溢出),请参见 samber/cc-skills-golang@golang-security 技能
  • → 有关调试恐慌和竞态条件,请参见 samber/cc-skills-golang@golang-troubleshooting 技能

常见错误

错误 修复
裸类型断言 v := x.(T) 类型不匹配时引发恐慌,导致程序崩溃。使用 v, ok := x.(T) 优雅处理
在接口函数中返回类型化 nil 接口持有 (type, nil) 不等于 nil。对于 nil 情况返回无类型 nil
向 nil map 写入 Nil map 没有底层存储——写入引发恐慌。使用 make(map[K]V) 初始化或惰性初始化
假设 append 总是复制 如果容量允许,两个切片共享底层数组。使用 s[:len(s):len(s)] 强制复制
循环中的 defer defer 在函数退出时执行,而非循环迭代——资源累积。将主体提取到单独函数
int64int32 无边界检查 值静默回绕(3B → -1.29B)。首先检查 math.MaxInt32/math.MinInt32
使用 == 比较浮点数 IEEE 754 表示不精确(0.1+0.2 != 0.3)。使用 math.Abs(a-b) < epsilon
整数除法无零检查 整数除以零引发恐慌。在除法前使用 if divisor == 0 保护
返回内部切片/映射引用 调用者可以通过共享的底层数组修改结构体的内部数据。返回防御性副本
多个 init() 且依赖顺序 init() 在文件间的执行顺序未指定。→ 参见 samber/cc-skills-golang@golang-design-patterns——使用显式构造函数
在 nil 通道上永远阻塞 Nil 通道在发送和接收时都会阻塞。始终在使用前初始化

交叉引用

  • → 有关使用这些指南进行自动化 AI 驱动的代码审查,请参见 samber/cc-skills-golang@golang-continuous-integration 技能