mcpField
G
go-error-handling

go-error-handling

github.com/cxuu开发1861.3k 次安装19 周前更新
代码仓库

关于 go-error-handling

面向 Go 开发者的错误处理技能,整理错误创建、包装、传播与处理的实践,并提供脚本检查常见反模式。

主要功能

  1. 01错误包装与类型选择
  2. 02错误传播与处理流程
  3. 03避免忽略错误
  4. 04脚本检测常见问题

适用场景

  1. 01设计可供调用方匹配的错误
  2. 02梳理错误应记录还是返回
  3. 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.md
ERROR-TYPES.md
WRAPPING.md
scripts/
check-errors-ast.go
check-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 中,错误是值——由代码创建,由代码使用。

选择错误策略

  1. 系统边界(RPC、IPC、存储)?→ 使用 %v 包装,避免泄露内部细节
  2. 调用方需要匹配特定条件?→ 使用哨兵错误或类型化错误,并用 %w 包装
  3. 调用方只需要调试上下文?→ fmt.Errorf("...: %w", err)
  4. 叶子函数,无需包装?→ 直接返回错误

默认:使用 %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,以便调用方判断上下文是否已取消。


处理错误

遇到错误时,应审慎选择处理方式——不要用 _ 丢弃:

  1. 立即处理——处理错误并继续
  2. 返回给调用方——可选择附加上下文进行包装
  3. 特殊情况下——使用 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

评论

支持 Markdown