go-error-handling
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseGo Error Handling Skill
Go错误处理技能
Operator Context
操作上下文
This skill operates as an operator for Go error handling implementation, configuring Claude's behavior for idiomatic, context-rich error propagation. It implements the Pattern Application architectural approach -- select the right error pattern (wrap, sentinel, custom type), apply it consistently, verify the error chain is preserved.
本技能用于指导Go错误处理的实现,配置Claude的行为以符合Go语言的惯用风格,实现带上下文的错误传播。它采用模式应用架构方法——选择合适的错误模式(包装、哨兵、自定义类型),一致地应用该模式,验证错误链是否被保留。
Hardcoded Behaviors (Always Apply)
硬编码行为(始终适用)
- CLAUDE.md Compliance: Read and follow repository CLAUDE.md before implementing error handling
- Over-Engineering Prevention: Use the simplest error pattern that fits. Do not create custom types when a sentinel suffices, or sentinels when a simple wrap is enough
- Always Check Errors: Every function that returns an error must have its error checked or explicitly ignored with
_ = fn() - Always Wrap with Context: Never use naked . Every wrap adds meaningful context describing the operation that failed
return err - Preserve Error Chains: Use verb in
%wso callers can usefmt.Errorfanderrors.Iserrors.As - Error Messages Form a Narrative: When read top-to-bottom, wrapped errors tell the story of what happened
- CLAUDE.md 合规性:在实现错误处理前,阅读并遵循仓库中的CLAUDE.md文档
- 避免过度设计:使用最适合当前场景的最简错误模式。当哨兵错误足够时,不要创建自定义类型;当简单包装足够时,不要使用哨兵错误
- 始终检查错误:每个返回错误的函数,其错误必须被检查,或通过显式忽略
_ = fn() - 始终带上下文包装:绝不要直接。每次包装都要添加描述失败操作的有意义上下文
return err - 保留错误链:在中使用
fmt.Errorf动词,以便调用方可以使用%w和errors.Iserrors.As - 错误消息形成完整叙事:从上到下阅读时,包装后的错误应能清晰描述事件发生的过程
Default Behaviors (ON unless disabled)
默认行为(默认开启,可关闭)
- Lower-case Error Messages: Error strings start lower-case, no trailing punctuation (Go convention)
- Contextual Identifiers: Include relevant IDs, keys, or filenames in wrap messages
- Sentinel Errors for API Boundaries: Define sentinel errors for conditions callers need to check
- Custom Types for Rich Context: Use custom error types only when callers need structured data
- errors.Is for Values, errors.As for Types: Never use string matching or type assertions for error checks
- HTTP Status Mapping: Map domain errors to HTTP status codes at the handler boundary
- 小写错误消息:错误字符串以小写开头,无末尾标点(Go语言惯例)
- 上下文标识符:在包装消息中包含相关的ID、键或文件名
- API边界使用哨兵错误:为调用方需要检查的条件定义哨兵错误
- 富上下文使用自定义类型:仅当调用方需要结构化数据时,才使用自定义错误类型
- errors.Is匹配值,errors.As匹配类型:绝不要使用字符串匹配或类型断言来检查错误
- HTTP状态码映射:在处理器边界将领域错误映射为HTTP状态码
Optional Behaviors (OFF unless enabled)
可选行为(默认关闭,可开启)
- gopls MCP Error Tracing: Use to find all usages of sentinel errors across the codebase,
go_symbol_referencesto verify error handling correctness after edits. Fallback:go_diagnosticsCLI or LSP toolgopls references - Error Wrapping Audit: Scan for naked statements and missing
return errverbs%w - Table-Driven Error Tests: Generate table-driven tests for error paths
- gopls MCP错误追踪:使用查找整个代码库中哨兵错误的所有用法,使用
go_symbol_references在编辑后验证错误处理的正确性。备选方案:go_diagnostics命令行工具或LSP工具gopls references - 错误包装审计:扫描直接的语句和缺失
return err动词的情况%w - 表格驱动的错误测试:为错误路径生成表格驱动的测试用例
Available Scripts
可用脚本
- — Detect bare
scripts/check-errors.shand log-and-return anti-patterns. Runreturn errfor options.bash scripts/check-errors.sh --help
- — 检测直接
scripts/check-errors.sh和「记录并返回」的反模式。运行return err查看可用选项。bash scripts/check-errors.sh --help
What This Skill CAN Do
本技能可实现的功能
- Guide idiomatic error wrapping with and
fmt.Errorf%w - Define and use sentinel errors (package-level vars)
errors.New - Create custom error types that implement the interface
error - Implement and
errors.Ischecks throughout error chainserrors.As - Map domain errors to HTTP status codes at handler boundaries
- Verify error propagation in table-driven tests
- 指导使用和
fmt.Errorf进行符合惯用风格的错误包装%w - 定义和使用哨兵错误(定义的包级变量)
errors.New - 创建实现接口的自定义错误类型
error - 在整个错误链中实现和
errors.Is检查errors.As - 在处理器边界将领域错误映射为HTTP状态码
- 在表格驱动的测试中验证错误传播
What This Skill CANNOT Do
本技能不可实现的功能
- Debug runtime panics or stack traces (use systematic-debugging instead)
- Implement structured logging (use logging-specific guidance instead)
- Handle Go concurrency error patterns (use go-concurrency instead)
- Design error monitoring or alerting systems (out of scope)
- 调试运行时panic或堆栈跟踪(请使用systematic-debugging技能)
- 实现结构化日志(请使用日志相关的指导)
- 处理Go并发错误模式(请使用go-concurrency技能)
- 设计错误监控或告警系统(超出本技能范围)
Instructions
操作步骤
Step 1: Identify the Error Pattern Needed
步骤1:确定所需的错误模式
Before writing any error handling code, determine which pattern fits:
| Situation | Pattern | Example |
|---|---|---|
| Operation failed, caller does not check type | Wrap with context | |
| Caller needs to check for specific condition | Sentinel error | |
| Caller needs structured error data | Custom error type | |
| Error at HTTP boundary | Status mapping | |
在编写任何错误处理代码之前,先确定适合当前场景的模式:
| 场景 | 模式 | 示例 |
|---|---|---|
| 操作失败,调用方无需检查错误类型 | 带上下文包装 | |
| 调用方需要检查特定条件 | 哨兵错误 | |
| 调用方需要结构化错误数据 | 自定义错误类型 | |
| HTTP边界处的错误 | 状态码映射 | |
Step 2: Wrap Errors with Context
步骤2:带上下文包装错误
Every error return should add context describing the operation that failed. Error messages form a readable narrative when chained.
go
func LoadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("load config from %s: %w", path, err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parse config JSON from %s: %w", path, err)
}
return &cfg, nil
}
// Output: "parse config JSON from /etc/app.json: invalid character..."Rules for wrap messages:
- Describe the operation, not the error (not
"load config")"error loading config" - Include identifying data (filename, ID, key)
- Use to preserve the error chain
%w - Start lower-case, no trailing punctuation
每个错误返回都应添加描述失败操作的上下文。链式包装后的错误应能形成可读的完整叙事。
go
func LoadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("load config from %s: %w", path, err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parse config JSON from %s: %w", path, err)
}
return &cfg, nil
}
// Output: "parse config JSON from /etc/app.json: invalid character..."包装消息规则:
- 描述操作而非错误本身(使用而非
"load config")"error loading config" - 包含识别信息(文件名、ID、键)
- 使用保留错误链
%w - 以小写开头,无末尾标点
Step 3: Define Sentinel Errors When Callers Need to Check
步骤3:为调用方需要检查的条件定义哨兵错误
Sentinel errors are package-level variables for conditions callers must handle specifically.
go
package mypackage
import "errors"
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
ErrInvalidInput = errors.New("invalid input")
)
func GetUser(ctx context.Context, id string) (*User, error) {
user, err := db.QueryUser(ctx, id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("query user %s: %w", id, err)
}
return user, nil
}
// Caller:
user, err := GetUser(ctx, id)
if errors.Is(err, ErrNotFound) {
// handle not found
}哨兵错误是包级变量,用于定义调用方必须专门处理的条件。
go
package mypackage
import "errors"
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
ErrInvalidInput = errors.New("invalid input")
)
func GetUser(ctx context.Context, id string) (*User, error) {
user, err := db.QueryUser(ctx, id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("query user %s: %w", id, err)
}
return user, nil
}
// Caller:
user, err := GetUser(ctx, id)
if errors.Is(err, ErrNotFound) {
// handle not found
}Step 4: Create Custom Error Types for Rich Context
步骤4:为富上下文创建自定义错误类型
Use custom types when callers need to extract structured data from errors.
go
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on field %q: %s", e.Field, e.Message)
}
func ValidateUser(u *User) error {
if u.Email == "" {
return &ValidationError{Field: "email", Message: "required"}
}
return nil
}
// Caller uses errors.As:
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf("Field %s failed: %s\n", valErr.Field, valErr.Message)
}当调用方需要从错误中提取结构化数据时,使用自定义类型。
go
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on field %q: %s", e.Field, e.Message)
}
func ValidateUser(u *User) error {
if u.Email == "" {
return &ValidationError{Field: "email", Message: "required"}
}
return nil
}
// Caller uses errors.As:
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf("Field %s failed: %s\n", valErr.Field, valErr.Message)
}Step 5: Use errors.Is and errors.As Correctly
步骤5:正确使用errors.Is和errors.As
errors.Is checks if any error in the chain matches a specific value:
go
if errors.Is(err, ErrNotFound) { /* handle */ }
if errors.Is(err, context.Canceled) { /* cancelled */ }
if errors.Is(err, os.ErrNotExist) { /* file missing */ }errors.As extracts a specific error type from the chain:
go
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Printf("Path: %s, Op: %s\n", pathErr.Path, pathErr.Op)
}
var netErr net.Error
if errors.As(err, &netErr) {
if netErr.Timeout() { /* handle timeout */ }
}errors.Is 检查错误链中是否存在特定值:
go
if errors.Is(err, ErrNotFound) { /* handle */ }
if errors.Is(err, context.Canceled) { /* cancelled */ }
if errors.Is(err, os.ErrNotExist) { /* file missing */ }errors.As 从错误链中提取特定类型的错误:
go
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Printf("Path: %s, Op: %s\n", pathErr.Path, pathErr.Op)
}
var netErr net.Error
if errors.As(err, &netErr) {
if netErr.Timeout() { /* handle timeout */ }
}Step 6: Map Errors to HTTP Status at Boundaries
步骤6:在边界处将错误映射为HTTP状态码
Error-to-status mapping belongs at the HTTP handler level, not in domain logic.
go
func errorToStatus(err error) int {
switch {
case errors.Is(err, ErrNotFound):
return http.StatusNotFound
case errors.Is(err, ErrUnauthorized):
return http.StatusUnauthorized
case errors.Is(err, ErrInvalidInput):
return http.StatusBadRequest
default:
return http.StatusInternalServerError
}
}错误到状态码的映射应在HTTP处理器层完成,而非领域逻辑层。
go
func errorToStatus(err error) int {
switch {
case errors.Is(err, ErrNotFound):
return http.StatusNotFound
case errors.Is(err, ErrUnauthorized):
return http.StatusUnauthorized
case errors.Is(err, ErrInvalidInput):
return http.StatusBadRequest
default:
return http.StatusInternalServerError
}
}Step 7: Test Error Paths
步骤7:测试错误路径
Use table-driven tests to verify error handling:
go
func TestProcessUser(t *testing.T) {
tests := []struct {
name string
input *User
wantErr error
}{
{
name: "nil user",
input: nil,
wantErr: ErrInvalidInput,
},
{
name: "missing email",
input: &User{Name: "test"},
wantErr: ErrInvalidInput,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ProcessUser(tt.input)
if tt.wantErr != nil {
if !errors.Is(err, tt.wantErr) {
t.Errorf("got error %v, want %v", err, tt.wantErr)
}
} else if err != nil {
t.Errorf("unexpected error: %v", err)
}
})
}
}使用表格驱动测试验证错误处理:
go
func TestProcessUser(t *testing.T) {
tests := []struct {
name string
input *User
wantErr error
}{
{
name: "nil user",
input: nil,
wantErr: ErrInvalidInput,
},
{
name: "missing email",
input: &User{Name: "test"},
wantErr: ErrInvalidInput,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ProcessUser(tt.input)
if tt.wantErr != nil {
if !errors.Is(err, tt.wantErr) {
t.Errorf("got error %v, want %v", err, tt.wantErr)
}
} else if err != nil {
t.Errorf("unexpected error: %v", err)
}
})
}
}Step 8: Verify Error Handling Completeness
步骤8:验证错误处理的完整性
Before completing, check:
- All errors checked -- no unchecked returns
- Context added -- each wrap describes the operation
- verb used -- error chain preserved
%w - Narrative formed -- error messages readable top-to-bottom
- Sentinel errors defined -- for conditions callers must check
- /
errors.Isused -- no string comparison or type assertionerrors.As - HTTP status mapped -- at handler boundaries only
完成前,请检查:
- 所有错误均已检查——无未检查的返回值
- 添加了上下文——每次包装都描述了操作
- 使用了动词——错误链已保留
%w - 形成了完整叙事——错误消息从上到下可读
- 定义了哨兵错误——针对调用方必须检查的条件
- 使用了/
errors.Is——无字符串比较或类型断言errors.As - 映射了HTTP状态码——仅在处理器边界处
Error Handling
错误处理
Error: "error chain broken -- errors.Is returns false"
错误:"error chain broken -- errors.Is returns false"
Cause: Used instead of in , or created a new error instead of wrapping
Solution:
%v%wfmt.Errorf- Check all calls use
fmt.Errorffor the error argument%w - Ensure sentinel errors are not re-created (use the same )
var - Verify custom types implement if they wrap inner errors
Unwrap()
原因:在中使用了而非,或创建了新错误而非包装原有错误
解决方案:
fmt.Errorf%v%w- 检查所有调用是否对错误参数使用了
fmt.Errorf%w - 确保哨兵错误未被重新创建(使用同一个定义的错误)
var - 如果自定义类型包装了内部错误,请验证其是否实现了方法
Unwrap()
Error: "redundant error context in logs"
错误:"redundant error context in logs"
Cause: Same context added at multiple call levels, or wrapping errors that already contain the info
Solution:
- Each level should add only its own context, not repeat caller context
- One wrap per call boundary -- do not double-wrap at the same level
原因:在多个调用层级添加了相同的上下文,或包装了已包含该信息的错误
解决方案:
- 每个层级应仅添加自身的上下文,不要重复调用方的上下文
- 每个调用边界仅包装一次——不要在同一层级重复包装
Error: "sentinel error comparison fails across packages"
错误:"sentinel error comparison fails across packages"
Cause: Error was recreated with instead of using the exported variable
Solution:
errors.New- Import and reference the package-level directly
var ErrX - Never shadow sentinel errors with local calls
errors.New
原因:使用重新创建了错误,而非引用导出的变量
解决方案:
errors.New- 直接导入并引用包级变量
var ErrX - 绝不要使用本地调用遮蔽哨兵错误
errors.New
Anti-Patterns
反模式
Anti-Pattern 1: Wrapping Without Meaningful Context
反模式1:无意义上下文的包装
What it looks like: or
Why wrong: "error" and "failed" add zero information. The chain becomes noise.
Do instead: Describe the operation:
return fmt.Errorf("error: %w", err)return fmt.Errorf("failed: %w", err)return fmt.Errorf("load user %s from database: %w", userID, err)表现: 或
问题:"error"和"failed"未添加任何有效信息,错误链会变得冗余嘈杂。
正确做法:描述具体操作:
return fmt.Errorf("error: %w", err)return fmt.Errorf("failed: %w", err)return fmt.Errorf("load user %s from database: %w", userID, err)Anti-Pattern 2: Naked Error Returns
反模式2:直接返回错误
What it looks like: without wrapping
Why wrong: Loses context at every call boundary. Final error message lacks the narrative chain.
Do instead: Always wrap:
return errreturn fmt.Errorf("outer operation: %w", err)表现:而不进行包装
问题:在每个调用边界丢失上下文,最终错误消息缺乏完整的事件叙事。
正确做法:始终进行包装:
return errreturn fmt.Errorf("outer operation: %w", err)Anti-Pattern 3: Silently Ignoring Errors
反模式3:静默忽略错误
What it looks like: without checking the return, or without comment
Why wrong: Silent failures cause data corruption, resource leaks, or hard-to-debug issues downstream.
Do instead: Check and log: or explicitly ignore:
file.Close()data, _ := fetchData()if err := file.Close(); err != nil { log.Printf("close file: %v", err) }_ = file.Close()表现:未检查返回值,或未添加注释
问题:静默失败会导致数据损坏、资源泄漏或下游难以调试的问题。
正确做法:检查并记录: 或显式忽略:
file.Close()data, _ := fetchData()if err := file.Close(); err != nil { log.Printf("close file: %v", err) }_ = file.Close()Anti-Pattern 4: String Matching for Error Checks
反模式4:使用字符串匹配检查错误
What it looks like:
Why wrong: Fragile. Error messages change. Breaks across wrapped errors.
Do instead: Use or
if strings.Contains(err.Error(), "not found")errors.Is(err, ErrNotFound)errors.As(err, &target)表现:
问题:脆弱易断。错误消息可能会变化,且在包装后的错误中会失效。
正确做法:使用 或
if strings.Contains(err.Error(), "not found")errors.Is(err, ErrNotFound)errors.As(err, &target)Anti-Pattern 5: Over-Wrapping at the Same Level
反模式5:同一层级过度包装
What it looks like:
Why wrong: Redundant wrapping creates unreadable error chains.
Do instead: One clear wrap per call level:
return fmt.Errorf("process: error processing: %w", fmt.Errorf("processing failed: %w", err))return fmt.Errorf("process user request: %w", err)表现:
问题:冗余包装会创建难以阅读的错误链。
正确做法:每个调用层级仅进行一次清晰的包装:
return fmt.Errorf("process: error processing: %w", fmt.Errorf("processing failed: %w", err))return fmt.Errorf("process user request: %w", err)References
参考资料
This skill uses these shared patterns:
- Anti-Rationalization - Prevents shortcut rationalizations
- Verification Checklist - Pre-completion checks
本技能使用以下共享模式:
- Anti-Rationalization - 避免捷径式的自我合理化
- Verification Checklist - 完成前的检查清单
Domain-Specific Anti-Rationalization
领域特定的反合理化
| Rationalization | Why It's Wrong | Required Action |
|---|---|---|
| "Simple error, no need to wrap" | Unwrapped errors lose context at every level | Always wrap with |
| "String check is fine for now" | String matching breaks when messages change | Use |
| "No one will check this error" | You cannot predict caller needs | Define sentinel if it crosses a package boundary |
| "Custom type is overkill" | Evaluate the actual need, but do not skip if callers need structured data | Match pattern to situation (Step 1 table) |
| 合理化借口 | 问题所在 | 要求操作 |
|---|---|---|
| "错误很简单,不需要包装" | 未包装的错误在每个层级都会丢失上下文 | 始终使用 |
| "现在用字符串检查没问题" | 当消息变化时,字符串匹配会失效 | 使用 |
| "没人会检查这个错误" | 无法预测调用方的需求 | 如果跨包边界,定义哨兵错误 |
| "自定义类型太小题大做" | 评估实际需求,但如果调用方需要结构化数据,不要跳过 | 根据场景匹配模式(步骤1的表格) |
Reference Files
参考文件
- : Extended patterns -- gopls tracing, HTTP handler patterns, error wrapping in middleware
${CLAUDE_SKILL_DIR}/references/patterns.md
- : 扩展模式——gopls追踪、HTTP处理器模式、中间件中的错误包装
${CLAUDE_SKILL_DIR}/references/patterns.md