go-error-handling

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Go 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
    return err
    . Every wrap adds meaningful context describing the operation that failed
  • Preserve Error Chains: Use
    %w
    verb in
    fmt.Errorf
    so callers can use
    errors.Is
    and
    errors.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.Is
    errors.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
    go_symbol_references
    to find all usages of sentinel errors across the codebase,
    go_diagnostics
    to verify error handling correctness after edits. Fallback:
    gopls references
    CLI or LSP tool
  • Error Wrapping Audit: Scan for naked
    return err
    statements and missing
    %w
    verbs
  • Table-Driven Error Tests: Generate table-driven tests for error paths
  • gopls MCP错误追踪:使用
    go_symbol_references
    查找整个代码库中哨兵错误的所有用法,使用
    go_diagnostics
    在编辑后验证错误处理的正确性。备选方案:
    gopls references
    命令行工具或LSP工具
  • 错误包装审计:扫描直接
    return err
    的语句和缺失
    %w
    动词的情况
  • 表格驱动的错误测试:为错误路径生成表格驱动的测试用例

Available Scripts

可用脚本

  • scripts/check-errors.sh
    — Detect bare
    return err
    and log-and-return anti-patterns. Run
    bash scripts/check-errors.sh --help
    for options.
  • scripts/check-errors.sh
    — 检测直接
    return err
    和「记录并返回」的反模式。运行
    bash scripts/check-errors.sh --help
    查看可用选项。

What This Skill CAN Do

本技能可实现的功能

  • Guide idiomatic error wrapping with
    fmt.Errorf
    and
    %w
  • Define and use sentinel errors (
    errors.New
    package-level vars)
  • Create custom error types that implement the
    error
    interface
  • Implement
    errors.Is
    and
    errors.As
    checks throughout error chains
  • 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:
SituationPatternExample
Operation failed, caller does not check typeWrap with context
fmt.Errorf("load config: %w", err)
Caller needs to check for specific conditionSentinel error
var ErrNotFound = errors.New("not found")
Caller needs structured error dataCustom error type
type ValidationError struct{...}
Error at HTTP boundaryStatus mapping
errors.Is(err, ErrNotFound) -> 404
在编写任何错误处理代码之前,先确定适合当前场景的模式:
场景模式示例
操作失败,调用方无需检查错误类型带上下文包装
fmt.Errorf("load config: %w", err)
调用方需要检查特定条件哨兵错误
var ErrNotFound = errors.New("not found")
调用方需要结构化错误数据自定义错误类型
type ValidationError struct{...}
HTTP边界处的错误状态码映射
errors.Is(err, ErrNotFound) -> 404

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 (
    "load config"
    not
    "error loading config"
    )
  • Include identifying data (filename, ID, key)
  • Use
    %w
    to preserve the error chain
  • 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
  • %w
    verb used -- error chain preserved
  • Narrative formed -- error messages readable top-to-bottom
  • Sentinel errors defined -- for conditions callers must check
  • errors.Is
    /
    errors.As
    used -- no string comparison or type assertion
  • 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
%v
instead of
%w
in
fmt.Errorf
, or created a new error instead of wrapping Solution:
  1. Check all
    fmt.Errorf
    calls use
    %w
    for the error argument
  2. Ensure sentinel errors are not re-created (use the same
    var
    )
  3. Verify custom types implement
    Unwrap()
    if they wrap inner errors
原因:在
fmt.Errorf
中使用了
%v
而非
%w
,或创建了新错误而非包装原有错误 解决方案:
  1. 检查所有
    fmt.Errorf
    调用是否对错误参数使用了
    %w
  2. 确保哨兵错误未被重新创建(使用同一个
    var
    定义的错误)
  3. 如果自定义类型包装了内部错误,请验证其是否实现了
    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:
  1. Each level should add only its own context, not repeat caller context
  2. One wrap per call boundary -- do not double-wrap at the same level
原因:在多个调用层级添加了相同的上下文,或包装了已包含该信息的错误 解决方案:
  1. 每个层级应仅添加自身的上下文,不要重复调用方的上下文
  2. 每个调用边界仅包装一次——不要在同一层级重复包装

Error: "sentinel error comparison fails across packages"

错误:"sentinel error comparison fails across packages"

Cause: Error was recreated with
errors.New
instead of using the exported variable Solution:
  1. Import and reference the package-level
    var ErrX
    directly
  2. Never shadow sentinel errors with local
    errors.New
    calls

原因:使用
errors.New
重新创建了错误,而非引用导出的变量 解决方案:
  1. 直接导入并引用包级
    var ErrX
    变量
  2. 绝不要使用本地
    errors.New
    调用遮蔽哨兵错误

Anti-Patterns

反模式

Anti-Pattern 1: Wrapping Without Meaningful Context

反模式1:无意义上下文的包装

What it looks like:
return fmt.Errorf("error: %w", err)
or
return fmt.Errorf("failed: %w", err)
Why wrong: "error" and "failed" add zero information. The chain becomes noise. Do instead: Describe the operation:
return fmt.Errorf("load user %s from database: %w", userID, err)
表现
return fmt.Errorf("error: %w", err)
return fmt.Errorf("failed: %w", err)
问题:"error"和"failed"未添加任何有效信息,错误链会变得冗余嘈杂。 正确做法:描述具体操作:
return fmt.Errorf("load user %s from database: %w", userID, err)

Anti-Pattern 2: Naked Error Returns

反模式2:直接返回错误

What it looks like:
return err
without wrapping Why wrong: Loses context at every call boundary. Final error message lacks the narrative chain. Do instead: Always wrap:
return fmt.Errorf("outer operation: %w", err)
表现
return err
而不进行包装 问题:在每个调用边界丢失上下文,最终错误消息缺乏完整的事件叙事。 正确做法:始终进行包装:
return fmt.Errorf("outer operation: %w", err)

Anti-Pattern 3: Silently Ignoring Errors

反模式3:静默忽略错误

What it looks like:
file.Close()
without checking the return, or
data, _ := fetchData()
without comment Why wrong: Silent failures cause data corruption, resource leaks, or hard-to-debug issues downstream. Do instead: Check and log:
if err := file.Close(); err != nil { log.Printf("close file: %v", err) }
or explicitly ignore:
_ = 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:
if strings.Contains(err.Error(), "not found")
Why wrong: Fragile. Error messages change. Breaks across wrapped errors. Do instead: Use
errors.Is(err, ErrNotFound)
or
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:
return fmt.Errorf("process: error processing: %w", fmt.Errorf("processing failed: %w", err))
Why wrong: Redundant wrapping creates unreadable error chains. Do instead: One clear wrap per call level:
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

领域特定的反合理化

RationalizationWhy It's WrongRequired Action
"Simple error, no need to wrap"Unwrapped errors lose context at every levelAlways wrap with
fmt.Errorf
and
%w
"String check is fine for now"String matching breaks when messages changeUse
errors.Is
or
errors.As
"No one will check this error"You cannot predict caller needsDefine sentinel if it crosses a package boundary
"Custom type is overkill"Evaluate the actual need, but do not skip if callers need structured dataMatch pattern to situation (Step 1 table)
合理化借口问题所在要求操作
"错误很简单,不需要包装"未包装的错误在每个层级都会丢失上下文始终使用
fmt.Errorf
%w
进行包装
"现在用字符串检查没问题"当消息变化时,字符串匹配会失效使用
errors.Is
errors.As
"没人会检查这个错误"无法预测调用方的需求如果跨包边界,定义哨兵错误
"自定义类型太小题大做"评估实际需求,但如果调用方需要结构化数据,不要跳过根据场景匹配模式(步骤1的表格)

Reference Files

参考文件

  • ${CLAUDE_SKILL_DIR}/references/patterns.md
    : Extended patterns -- gopls tracing, HTTP handler patterns, error wrapping in middleware
  • ${CLAUDE_SKILL_DIR}/references/patterns.md
    : 扩展模式——gopls追踪、HTTP处理器模式、中间件中的错误包装