mcpField
G
go-error-handling

go-error-handling

github.com/eduardo-sl开发86214 次安装3 周前更新
代码仓库

关于 go-error-handling

go-error-handling 是面向 Go 开发者和编码 Agent 的开发技能,说明错误的创建、包装、传递与对外发布方式,强调上下文、错误链和系统边界处理。

主要功能

  1. 01哨兵错误与自定义错误模式
  2. 02添加上下文并保留错误链
  3. 03日志、返回错误与 panic 规则
  4. 04边界转换与稳定错误消息
  5. 05go vet 和 golangci-lint 验证

适用场景

  1. 01设计调用方可识别的错误
  2. 02带有用上下文传递故障
  3. 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. 错误决策树

创建或返回错误时,遵循以下决策树:

  1. 简单,不需要额外上下文? → errors.New("message")
  2. 需要为现有错误补充上下文? → fmt.Errorf("doing X: %w", err)
  3. 调用方需要识别此错误? → 哨兵 var 或自定义类型
  4. 错误携带结构化数据? → 实现 error 的自定义类型
  5. 从下游传递错误? → 使用 %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

验证清单

  1. 不使用 _ 丢弃错误(除非在注释中明确说明理由)
  2. 每个 fmt.Errorf 包装都使用 %w(或使用 %v 并记录原因)
  3. 哨兵错误使用 var Err... 命名
  4. 自定义错误类型实现 error 接口
  5. 调用方使用 errors.Is / errors.As(Go 1.26+ 使用 errors.AsType),绝不使用 == 或类型断言
  6. 包装原因的自定义类型实现 Unwrap() error
  7. 不使用“记录日志后返回”模式
  8. 错误消息使用小写,包含上下文,并便于错误链处理
  9. 不在面向用户的响应中包含 err.Error() 文本;日志消息保持稳定,详细信息作为属性记录

评论

支持 Markdown