Persona: You are a defensive Go engineer. You treat every untested assumption about nil, capacity, and numeric range as a latent crash waiting to happen.
Go Safety: Correctness & Defensive Coding
Prevents programmer mistakes — bugs, panics, and silent data corruption in normal (non-adversarial) code. Security handles attackers; safety handles ourselves.
Best Practices Summary
- 1. Prefer generics over
any when the type set is known — compiler catches mismatches instead of runtime panics - Always use comma-ok for type assertions — bare assertions panic on mismatch
- Typed nil pointer in an interface is not
== nil — the type descriptor makes it non-nil - Writing to a nil map panics — always initialize before use
append may reuse the backing array — both slices share memory if capacity allows, silently corrupting each other- Return defensive copies from exported functions — otherwise callers mutate your internals
defer runs at function exit, not loop iteration — extract loop body to a function- Integer conversions truncate silently —
int64 to int32 wraps without error - Float arithmetic is not exact — use epsilon comparison or INLINECODE6
- Design useful zero values — nil map fields panic on first write; use lazy init
- Use
sync.Once for lazy init — guarantees exactly-once even under concurrency
Nil Safety
Nil-related panics are the most common crash in Go.
The nil interface trap
Interfaces store (type, value). An interface is nil only when both are nil. Returning a typed nil pointer sets the type descriptor, making it non-nil:
CODEBLOCK0
Nil map, slice, and channel behavior
| Type | Read from nil | Write to nil | Len/Cap of nil | Range over nil |
|---|
| Map | Zero value | panic | 0 | 0 iterations |
| Slice |
panic (index) |
panic (index) | 0 | 0 iterations |
| Channel | Blocks forever | Blocks forever | 0 | Blocks forever |
CODEBLOCK1
See Nil Safety Deep Dive for nil receivers, nil in generics, and nil interface performance.
Slice & Map Safety
Slice aliasing — the append trap
INLINECODE9 reuses the backing array if capacity allows. Both slices then share memory:
CODEBLOCK2
Map concurrent access
Maps MUST NOT be accessed concurrently — → see samber/cc-skills-golang@golang-concurrency for sync primitives.
See Slice and Map Deep Dive for range pitfalls, subslice memory retention, and slices.Clone/maps.Clone.
Numeric Safety
Implicit type conversions truncate silently
CODEBLOCK3
Float comparison
CODEBLOCK4
Division by zero
Integer division by zero panics. Float division by zero produces +Inf, -Inf, or NaN.
CODEBLOCK5
For integer overflow as a security vulnerability, see the samber/cc-skills-golang@golang-security skill section.
Resource Safety
defer in loops — resource accumulation
INLINECODE17 runs at function exit, not loop iteration. Resources accumulate until the function returns:
CODEBLOCK6
Goroutine leaks
→ See samber/cc-skills-golang@golang-concurrency for goroutine lifecycle and leak prevention.
Immutability & Defensive Copying
Exported functions returning slices/maps SHOULD return defensive copies.
Protecting struct internals
CODEBLOCK7
Initialization Safety
Zero-value design
Design types so var x MyType is safe — prevents "forgot to initialize" bugs:
CODEBLOCK8
sync.Once for lazy initialization
CODEBLOCK9
init() function pitfalls
→ See samber/cc-skills-golang@golang-design-patterns for why init() should be avoided in favor of explicit constructors.
Enforce with Linters
Many safety pitfalls are caught automatically by linters: errcheck, forcetypeassert, nilerr, govet, staticcheck. See the samber/cc-skills-golang@golang-linter skill for configuration and usage.
Cross-References
- - → See
samber/cc-skills-golang@golang-concurrency skill for concurrent access patterns and sync primitives - → See
samber/cc-skills-golang@golang-data-structures skill for slice/map internals, capacity growth, and container/ packages - → See
samber/cc-skills-golang@golang-error-handling skill for nil error interface trap - → See
samber/cc-skills-golang@golang-security skill for security-relevant safety issues (memory safety, integer overflow) - → See
samber/cc-skills-golang@golang-troubleshooting skill for debugging panics and race conditions
Common Mistakes
| Mistake | Fix |
|---|
| Bare type assertion INLINECODE32 | Panics on type mismatch, crashing the program. Use v, ok := x.(T) to handle gracefully |
| Returning typed nil in interface function |
Interface holds (type, nil) which is != nil. Return untyped
nil for the nil case |
| Writing to a nil map | Nil maps have no backing storage — write panics. Initialize with
make(map[K]V) or lazy-init |
| Assuming
append always copies | If capacity allows, both slices share the backing array. Use
s[:len(s):len(s)] to force a copy |
|
defer in a loop |
defer runs at function exit, not loop iteration — resources accumulate. Extract body to a separate function |
|
int64 to
int32 without bounds check | Values wrap silently (3B → -1.29B). Check against
math.MaxInt32/
math.MinInt32 first |
| Comparing floats with
== | IEEE 754 representation is not exact (
0.1+0.2 != 0.3). Use
math.Abs(a-b) < epsilon |
| Integer division without zero check | Integer division by zero panics. Guard with
if divisor == 0 before dividing |
| Returning internal slice/map reference | Callers can mutate your struct's internals through the shared backing array. Return a defensive copy |
| Multiple
init() with ordering assumptions |
init() execution order across files is unspecified. → See
samber/cc-skills-golang@golang-design-patterns — use explicit constructors |
| Blocking forever on nil channel | Nil channels block on both send and receive. Always initialize before use |
技能名称: golang-safety
角色定位: 你是一名防御性Go工程师。你将每一个关于nil、容量和数值范围的未经测试的假设,都视为潜在的崩溃隐患。
Go 安全性:正确性与防御性编码
防止程序员错误——在正常(非对抗性)代码中出现的bug、panic和静默数据损坏。安全性处理攻击者;安全性处理我们自己。
最佳实践总结
- 1. 当类型集合已知时,优先使用泛型而非 any —— 编译器能捕获类型不匹配,而非在运行时panic
- 始终对类型断言使用逗号-ok模式 —— 裸断言在类型不匹配时会panic
- 接口中的类型化nil指针不等于 == nil —— 类型描述符使其非nil
- 向nil map写入会panic —— 使用前务必初始化
- append 可能重用底层数组 —— 如果容量允许,两个切片会共享内存,静默地相互破坏
- 从导出函数返回防御性副本 —— 否则调用者会修改你的内部状态
- defer 在函数退出时执行,而非循环迭代时 —— 将循环体提取到一个函数中
- 整数转换会静默截断 —— int64 转 int32 会无错误地回绕
- 浮点数运算不精确 —— 使用epsilon比较或 math/big
- 设计有用的零值 —— nil map字段在首次写入时panic;使用惰性初始化
- 使用 sync.Once 进行惰性初始化 —— 即使在并发下也能保证只执行一次
Nil 安全性
与nil相关的panic是Go中最常见的崩溃。
nil接口陷阱
接口存储(类型,值)。只有当两者都为nil时,接口才为 nil。返回一个类型化的nil指针会设置类型描述符,使其非nil:
go
// ✗ 危险 — 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、切片和channel的行为
| 类型 | 从nil读取 | 向nil写入 | nil的Len/Cap | 对nil进行Range |
|---|
| Map | 零值 | panic | 0 | 0次迭代 |
| 切片 |
panic (索引) |
panic (索引) | 0 | 0次迭代 |
| Channel | 永久阻塞 | 永久阻塞 | 0 | 永久阻塞 |
go
// ✗ 糟糕 — 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 安全性深度探讨。
切片与Map安全性
切片别名——append陷阱
如果容量允许,append 会重用底层数组。这样两个切片会共享内存:
go
// ✗ 危险 — 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。
关于range陷阱、子切片内存保留以及 slices.Clone/maps.Clone,请参阅 切片与Map深度探讨。
数值安全性
隐式类型转换会静默截断
go
// ✗ 糟糕 — 如果 val > math.MaxInt32 (30亿变成-12.9亿),会静默回绕
var val int64 = 3000000_000
i32 := int32(val) // -1294967296 (静默回绕)
// ✓ 良好 — 在转换前检查
if val > math.MaxInt32 || val < math.MinInt32 {
return fmt.Errorf(值 %d 溢出 int32, val)
}
i32 := int32(val)
浮点数比较
go
// ✗ 糟糕 — 浮点数运算不精确
0.1+0.2 == 0.3 // false
// ✓ 良好 — 使用epsilon比较
const epsilon = 1e-9
math.Abs((0.1+0.2)-0.3) < epsilon // true
除零错误
整数除以零会panic。浮点数除以零会产生 +Inf、-Inf 或 NaN。
go
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 在函数退出时执行,而非循环迭代时。资源会一直累积直到函数返回:
go
// ✗ 糟糕 — 所有文件保持打开状态直到函数返回
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。
不可变性与防御性拷贝
返回切片/map的导出函数应该返回防御性副本。
保护结构体内部状态
go
// ✗ 糟糕 — 导出的切片字段,任何人都可以修改
type Config struct {
Hosts []string
}
// ✓ 良好 — 未导出字段,通过返回副本的访问器访问
type Config struct {
hosts []string
}
func (c *Config) Hosts() []string {
return slices.Clone(c.hosts)
}
初始化安全性
零值设计
设计类型使得 var x MyType 是安全的——防止忘记初始化的bug:
go
var mu sync.Mutex // ✓ 零值可用
var buf bytes.Buffer // ✓ 零值可用
// ✗ 糟糕 — nil map在写入时panic
type Cache struct { data map[string]any }
使用sync.Once进行惰性初始化
go
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自动捕获:errcheck、forcetypeassert、nilerr、govet、staticcheck。关于配置和使用,请参见 samber/cc-skills-golang@golang-linter 技能。
交叉引用
- - → 关于并发访问模式和同步原语,请参见 samber/cc-skills-golang@golang-concurrency 技能
- → 关于切片/map内部机制、容量增长和container/包,请参见 samber/cc-skills-golang@golang-data-structures 技能
- → 关于nil错误接口陷阱,请参见 samber/cc-skills-golang@golang-error-handling 技能
- → 关于安全相关的安全问题(内存安全、整数