关于 go-error-handling
go-error-handling 是面向 Go 开发者和编码 Agent 的开发技能,说明错误的创建、包装、传递与对外发布方式,强调上下文、错误链和系统边界处理。
主要功能
- 01哨兵错误与自定义错误模式
- 02添加上下文并保留错误链
- 03日志、返回错误与 panic 规则
- 04边界转换与稳定错误消息
- 05go vet 和 golangci-lint 验证
适用场景
- 01设计调用方可识别的错误
- 02带有用上下文传递故障
- 03审查 API 边界的错误处理
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ eduardo-sl/ go-agent-skills --skill go-error-handling安装后在 Claude Code 中输入/go-error-handling确认已加载。
附带文件references/patterns-and-boundaries.md
详细介绍
Go 错误处理
Go 显式处理错误是一项特性,而非限制。 这些模式可确保错误信息明确、可操作,并得到妥善传递。
社区默认规范。如果项目技能或 Agent 配置规则明确覆盖了
go-error-handling,则优先遵循该规则。
详细参考资料,按需加载:
references/patterns-and-boundaries.md— 错误检查模式、Is方法、在 HTTP 边界转换错误、低基数日志记录,以及 Go 1.27 中针对%w与指针错误的 vet 检查。
仅当下方摘要不够时才读取参考文件。
1. 错误决策树
创建或返回错误时,遵循以下决策树:
- 简单,不需要额外上下文? →
errors.New("message") - 需要为现有错误补充上下文? →
fmt.Errorf("doing X: %w", err) - 调用方需要识别此错误? → 哨兵
var或自定义类型 - 错误携带结构化数据? → 实现
error的自定义类型 - 从下游传递错误? → 使用
%w包装并补充上下文
2. 哨兵错误
对于调用方需要检查的错误,使用包级 var:
// ✅ Good — exported sentinel error
var (
ErrNotFound = errors.New("user: not found")
ErrUnauthorized = errors.New("user: unauthorized")
)
// Naming convention: Err + Description
// Prefix with package context in the message
调用方使用 errors.Is 检查:
if errors.Is(err, user.ErrNotFound) {
// handle not found
}
绝不要使用 == 比较错误。始终使用 errors.Is()。
3. 自定义错误类型
当错误需要携带结构化信息时:
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation: field %s: %s", e.Field, e.Message)
}
// Callers extract it from the chain — Go 1.26+:
if valErr, ok := errors.AsType[*ValidationError](err); ok {
log.Printf("invalid field: %s", valErr.Field)
}
// Before Go 1.26:
var valErr *ValidationError
if errors.As(err, &valErr) {
log.Printf("invalid field: %s", valErr.Field)
}
包装原因的自定义类型必须公开该原因,否则 errors.Is/errors.As
会止步于你的类型,无法看到底层错误:
type QueryError struct {
Query string
Err error
}
func (e *QueryError) Error() string { return "query " + e.Query + ": " + e.Err.Error() }
func (e *QueryError) Unwrap() error { return e.Err } // keeps the chain walkable
4. 错误包装
向上层传递错误时,始终补充上下文。
使用 %w 保留错误链:
// ✅ Good — context added, chain preserved
func getUser(id int64) (*User, error) {
row, err := db.QueryRow(ctx, query, id)
if err != nil {
return nil, fmt.Errorf("get user %d: %w", id, err)
}
// ...
}
// ❌ Bad — no context
return nil, err
// ❌ Bad — chain broken, callers can't errors.Is/As
return nil, fmt.Errorf("failed: %v", err)
何时不使用 %w
当你明确希望断开错误链,避免调用方依赖内部实现错误时,使用 %v 而非 %w:
// Intentionally hiding internal DB error from public API
return nil, fmt.Errorf("user lookup failed: %v", err)
5. 每个错误只处理一次
错误要么记录日志,要么返回,绝不能两者兼而有之:
// ✅ Good — return the error, let caller decide
func loadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("load config %s: %w", path, err)
}
// ...
}
// ❌ Bad — log AND return (error handled twice)
func loadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
log.Printf("failed to read config: %v", err) // handled once
return nil, err // handled again
}
// ...
}
规则:由决定如何处理错误的组件负责记录日志/指标。其他组件只负责包装并返回。
6. 错误命名约定
// Sentinel errors: Err prefix
var ErrNotFound = errors.New("not found")
// Error types: Error suffix
type NotFoundError struct { ... }
type ValidationError struct { ... }
// Error messages: lowercase, no punctuation, no "failed to" prefix
// Include context: "package: action: detail"
errors.New("auth: token expired")
fmt.Errorf("user: get by id %d: %w", id, err)
7. Panic 规则
Panic 不是错误处理。仅在以下情况使用 panic:
- 程序初始化失败且无法继续(
template.Must、标志解析) - 不应发生的编程错误(违反不变量)
- 空指针解引用表明存在错误,而非运行时状况
在测试中使用 t.Fatal / t.FailNow,绝不要使用 panic。
在 HTTP 处理程序和中间件中,应在边界处从 panic 中恢复,避免单个请求导致服务器崩溃。
8. 错误检查模式
- 如果没有其他地方需要该错误,就将其作用域限定在
if中:if err := do(); err != nil { return fmt.Errorf("do: %w", err) }。 - 对具名
err结果使用延迟包装,可以为长函数的每条返回路径添加同一个前缀。 - 收集相互独立的失败(验证、并行工作),并返回
errors.Join(errs...)—errors.Is/As可以识别每个合并后的错误。
9. 边界处的错误
- 公开错误是一种契约。 在模块内部,使用
%w包装。 在你公开的边界(导出的库 API、RPC)处,%w会使被包装的错误成为 API 的一部分;使用%v或创建新错误可将其保留为内部实现细节。 - 在错误到达用户之前进行转换。 在处理程序处将领域错误映射为状态码和稳定消息,并连同错误链只记录一次原始错误。响应中的
err.Error()会泄露 SQL、路径和主机名。 - 保持日志消息的基数较低。 稳定文本就是消息;ID、路径和计数应放在结构化属性中,以便日志后端对相同失败进行分组。
- Panic 不会跨 goroutine 传播。
recover只能在发生 panic 的 goroutine 中生效 — 参见 go-concurrency-review。 - 在某个类型上实现
Is(target error) bool,可让errors.Is将一类错误匹配到同一个哨兵错误。
第 8 和第 9 节的示例:references/patterns-and-boundaries.md。
可执行验证
编写或审查错误处理后,运行能够验证上述规则的 linter(未安装的可跳过并注明):
go vet ./... # includes printf %w misuse
golangci-lint run --enable errcheck,errorlint # unchecked errors, %v-vs-%w,
# == comparisons on errors
验证清单
- 不使用
_丢弃错误(除非在注释中明确说明理由) - 每个
fmt.Errorf包装都使用%w(或使用%v并记录原因) - 哨兵错误使用
var Err...命名 - 自定义错误类型实现
error接口 - 调用方使用
errors.Is/errors.As(Go 1.26+ 使用errors.AsType),绝不使用==或类型断言 - 包装原因的自定义类型实现
Unwrap() error - 不使用“记录日志后返回”模式
- 错误消息使用小写,包含上下文,并便于错误链处理
- 不在面向用户的响应中包含
err.Error()文本;日志消息保持稳定,详细信息作为属性记录