关于 go-error-handling
面向 Go 开发者的错误处理技能,整理错误创建、包装、传播与处理的实践,并提供脚本检查常见反模式。
主要功能
- 01错误包装与类型选择
- 02错误传播与处理流程
- 03避免忽略错误
- 04脚本检测常见问题
适用场景
- 01设计可供调用方匹配的错误
- 02梳理错误应记录还是返回
- 03检查 Go 代码中的错误处理模式
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ cxuu/ golang-skills --skill go-error-handling安装后在 Claude Code 中输入/go-error-handling确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/ERROR-FLOW.mdERROR-TYPES.mdWRAPPING.mdscripts/check-errors-ast.gocheck-errors.sh详细介绍
Go 错误处理
兼容性:
errors.Is、errors.As和%w包装要求 Go 1.13+;结构化日志示例可能会使用 Go 1.21+ 中的log/slog。
资源路由
scripts/check-errors.sh- 检查基于字符串的错误匹配、直接传递错误以及记录日志后返回的模式时运行。scripts/check-errors-ast.go- 由check-errors.sh调用的实现辅助程序;更改错误流分析行为时,请修改此文件。references/ERROR-FLOW.md- 决定在哪里处理、包装、记录或返回错误时阅读。references/ERROR-TYPES.md- 选择哨兵错误、类型化错误或不透明错误时阅读。references/WRAPPING.md- 选择%w还是%v,或跨越包边界时阅读。
在 Go 中,错误是值——由代码创建,由代码使用。
选择错误策略
- 系统边界(RPC、IPC、存储)?→ 使用
%v包装,避免泄露内部细节 - 调用方需要匹配特定条件?→ 使用哨兵错误或类型化错误,并用
%w包装 - 调用方只需要调试上下文?→
fmt.Errorf("...: %w", err) - 叶子函数,无需包装?→ 直接返回错误
默认:使用 %w 包装,并将其放在格式字符串末尾。
核心规则
切勿返回具体错误类型
导出函数切勿返回具体错误类型——具体的 nil
指针可能会变成非 nil 接口:
// Bad: Concrete type can cause subtle bugs
func Bad() *os.PathError { /*...*/ }
// Good: Always return the error interface
func Good() error { /*...*/ }
错误字符串
错误字符串不应大写,也不应以标点符号结尾。 例外:导出名称、专有名词或首字母缩写词。
// Bad
err := fmt.Errorf("Something bad happened.")
// Good
err := fmt.Errorf("something bad happened")
对于显示的消息(日志、测试失败信息、API 响应),使用大写是恰当的。
出错时的返回值
函数返回错误时,除非有明确文档说明,否则调用方必须将所有非错误返回值视为未指定。
提示:接受 context.Context 的函数通常应返回 error,以便调用方判断上下文是否已取消。
处理错误
遇到错误时,应审慎选择处理方式——不要用 _ 丢弃:
- 立即处理——处理错误并继续
- 返回给调用方——可选择附加上下文进行包装
- 特殊情况下——使用
log.Fatal或panic
要有意忽略错误时,请添加注释说明原因。
n, _ := b.Write(p) // never returns a non-nil error
对于相关的并发操作,请使用
errgroup:
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error { return task1(ctx) })
g.Go(func() error { return task2(ctx) })
if err := g.Wait(); err != nil { return err }
避免带内错误
不要返回 -1、nil 或空字符串来表示错误。应使用多个返回值:
// Bad: In-band error value
func Lookup(key string) int // returns -1 for missing
// Good: Explicit error or ok value
func Lookup(key string) (string, bool)
这样可以防止调用方编写 Parse(Lookup(key))——由于 Lookup(key) 有 2 个输出,这会导致编译时错误。
错误流
在正常代码之前处理错误。提前返回可避免正常路径增加缩进:
// Good: Error first, normal code unindented
if err != nil {
return err
}
// normal code
错误只处理一次——要么记录日志,要么返回,绝不可两者兼用:
Error encountered?
├─ Caller can act on it? → Return (with context via %w)
├─ Top of call chain? → Log and handle
└─ Neither? → Log at appropriate level, continue
错误类型
建议:推荐的最佳实践。
| 调用方需要匹配? | 消息类型 | 用法 |
|---|---|---|
| 否 | 静态 | errors.New("message") |
| 否 | 动态 | fmt.Errorf("msg: %v", val) |
| 是 | 静态 | var ErrFoo = errors.New("...") |
| 是 | 动态 | 自定义 error 类型 |
默认:使用 fmt.Errorf("...: %w", err) 包装。需要 errors.Is() 时升级为哨兵错误,需要 errors.As() 时升级为自定义类型。
错误包装
建议:推荐的最佳实践。
- 使用
%v:在系统边界处、记录日志时使用,以隐藏内部细节 - 使用
%w:保留错误链,以供errors.Is/errors.As使用
关键规则:将 %w 放在末尾。添加调用方尚不了解的上下文。如果注释没有增加任何内容,请直接返回 err。
验证:实现错误处理后,运行
bash scripts/check-errors.sh检测常见反模式。然后运行go vet ./...以发现其他问题。
相关技能
- 错误命名:命名哨兵错误(
ErrFoo)或自定义错误类型时,请参阅 go-naming - 错误测试:使用
errors.Is/errors.As测试错误语义或编写错误检查辅助函数时,请参阅 go-testing - Panic 处理:决定使用 panic 还是返回错误,或编写 recover 防护时,请参阅 go-defensive
- 守卫子句:组织提前返回的错误流程或减少嵌套时,请参阅 go-control-flow
- 日志记录决策:选择日志级别、配置结构化日志,或决定在日志消息中包含哪些上下文时,请参阅 go-logging