关于 go-defensive
go-defensive 汇总 Go API 边界处的防御式编程实践,面向希望降低数据别名、资源泄漏和错误处理风险的开发者。指南涉及复制切片与映射、清理资源、枚举、随机数及 panic 处理。
主要功能
- 01在 API 边界复制切片与映射
- 02使用 defer 清理资源
- 03避免可变全局状态
- 04建议枚举从非零值开始
- 05区分安全随机数与普通随机数
- 06限制 panic 并规范 recover
适用场景
- 01审查 API 边界的数据安全
- 02检查文件、锁与取消操作的清理
- 03评估枚举、全局状态和 panic 用法
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ cxuu/ golang-skills --skill go-defensive安装后在 Claude Code 中输入/go-defensive确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/BOUNDARY-COPYING.mdGLOBAL-STATE.mdMUST-FUNCTIONS.mdPANIC-RECOVER.mdTIME-ENUMS-TAGS.md详细介绍
Go 防御式编程模式
兼容性:加密示例可能使用
crypto/rand.Text,这需要 Go 1.24+。
资源路由
references/BOUNDARY-COPYING.md- 跨 API 边界复制切片/映射时阅读。references/GLOBAL-STATE.md- 引入或移除包级全局变量时阅读。references/MUST-FUNCTIONS.md- 判断是否可以接受遇错即 panic 的辅助函数时阅读。references/PANIC-RECOVER.md- 评估 panic、recover 或崩溃隔离时阅读。references/TIME-ENUMS-TAGS.md- 处理时间类型、枚举零值或结构体标签时阅读。
防御式检查清单优先级
在 API 边界加固代码时,按以下顺序检查:
Reviewing an API boundary?
├─ 1. Error handling → Return errors; don't panic (see go-error-handling)
├─ 2. Input validation → Copy slices/maps received from callers
├─ 3. Output safety → Copy slices/maps before returning to callers
├─ 4. Resource cleanup → Use defer for Close/Unlock/Cancel
├─ 5. Interface checks → Route compile-time assertions to go-interfaces
├─ 6. Time correctness → Use time.Time and time.Duration, not int/float
├─ 7. Enum safety → Start iota at 1 so zero-value is invalid
└─ 8. Crypto safety → crypto/rand for keys, never math/rand
快速参考
| 模式 | 规则 | 详情 |
|---|---|---|
| 边界复制 | 接收和返回时复制切片/映射 | BOUNDARY-COPYING.md |
| 延迟清理 | 在 os.Open 后立即使用 defer f.Close() | 见下文 |
| 接口检查 | 编译时接口实现断言 | 参见 go-interfaces |
| 时间类型 | 使用 time.Time / time.Duration,绝不使用原始 int | TIME-ENUMS-TAGS.md |
| 枚举起始值 | 使用 iota + 1,让零值表示无效 | 见下文 |
| 加密随机数 | 密钥使用 crypto/rand,绝不使用 math/rand | 见下文 |
| Must 函数 | 仅在初始化时使用;失败时 panic | MUST-FUNCTIONS.md |
| Panic/recover | 绝不跨包暴露 panic | PANIC-RECOVER.md |
| 可变全局变量 | 改用依赖注入 | 见下文 |
验证接口实现
将编译时接口实现断言交由 go-interfaces 处理。 仅使用此技能来识别 API 边界的健壮性风险;何时适合使用断言以及断言的具体形式由接口技能负责。
在边界处复制切片和映射
切片和映射包含指向底层数据的指针。在 API 边界处进行复制,以防止意外修改。
// Receiving: copy incoming slice
d.trips = make([]Trip, len(trips))
copy(d.trips, trips)
// Returning: copy map before returning
result := make(map[string]int, len(s.counters))
for k, v := range s.counters { result[k] = v }
使用 Defer 进行清理
使用 defer 清理资源(文件、锁)。这样可以避免在多个返回路径中遗漏清理。
p.Lock()
defer p.Unlock()
if p.count < 10 {
return p.count
}
p.count++
return p.count
defer 的开销可以忽略不计。为清晰起见,在 os.Open 后立即放置
defer f.Close()。延迟函数的参数会在执行 defer 时求值,而不是在
函数运行时求值。多个 defer 按后进先出顺序执行。
结构体字段标签
建议:始终为需要编组或解组的结构体添加显式字段标签。
type User struct {
Name string `json:"name" yaml:"name"`
Email string `json:"email" yaml:"email"`
}
字段标签是序列化契约——重命名结构体字段却不更新标签,会悄无声息地破坏线路兼容性。对于任何跨越序列化边界的类型,都应将标签视为公共 API 的一部分。
将枚举起始值设为一
将枚举起始值设为非零,以区分未初始化值和有效值。
const (
Add Operation = iota + 1 // Add=1, zero value = uninitialized
Subtract
Multiply
)
例外:零值是合理默认值时(例如,LogToStdout = iota)。
时间、结构体标签和嵌入
避免可变全局变量
注入依赖,而不是修改包级变量。这样无需保存和恢复全局状态也能测试代码。
type signer struct {
now func() time.Time // injected; tests replace with fixed time
}
func newSigner() *signer {
return &signer{now: time.Now}
}
加密随机数
生成密钥时不要使用 math/rand 或 math/rand/v2——这是一个安全问题。以时间为种子的生成器会产生可预测的输出。
import "crypto/rand"
func Key() string { return rand.Text() }
对于文本输出,直接使用 crypto/rand.Text,或使用 encoding/hex 或 encoding/base64 编码随机字节。
Panic 和 Recover
仅在确实无法恢复的情况下使用 panic。库函数应避免使用 panic。
func safelyDo(work *Work) {
defer func() {
if err := recover(); err != nil {
log.Println("work failed:", err)
}
}()
do(work)
}
关键规则:
- 绝不跨包暴露 panic——始终将其转换为错误
- 如果库确实无法完成自身初始化,可以在
init()中 panic - 使用 recover 在服务器 goroutine 处理程序中隔离 panic
Must 函数
Must 函数会在出错时 panic——仅在程序初始化期间使用,此时失败意味着程序无法运行。
var validID = regexp.MustCompile(`^[a-z][a-z0-9-]{0,62}$`)
var tmpl = template.Must(template.ParseFiles("index.html"))
相关技能
- 错误处理:在选择返回错误还是触发 panic,或在边界处包装错误时,请参阅 go-error-handling
- 并发安全:在使用互斥锁、原子操作或通道保护共享状态时,请参阅 go-concurrency
- 接口检查:在添加编译时接口实现检查时,请参阅 go-interfaces
- 数据结构复制:在处理切片/映射内部结构或指针别名时,请参阅 go-data-structures