go-error-handling

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Go Error Handling

Go错误处理

Go's explicit error handling is a feature, not a limitation. These patterns ensure errors are informative, actionable, and properly propagated.
Go的显式错误处理是一项特性,而非局限。 这些模式可确保错误信息丰富、可操作且能正确传播。

1. Error Decision Tree

1. 错误决策树

When creating or returning an error, follow this tree:
  1. Simple, no extra context needed?
    errors.New("message")
  2. Need to add context to existing error?
    fmt.Errorf("doing X: %w", err)
  3. Caller needs to detect this error? → Sentinel
    var
    or custom type
  4. Error carries structured data? → Custom type implementing
    error
  5. Propagating from downstream? → Wrap with
    %w
    and add context
当创建或返回错误时,请遵循以下决策流程:
  1. 简单场景,无需额外上下文?
    errors.New("message")
  2. 需要为现有错误添加上下文?
    fmt.Errorf("doing X: %w", err)
  3. 调用方需要检测该错误? → 哨兵
    var
    变量或自定义类型
  4. 错误需要携带结构化数据? → 实现
    error
    接口的自定义类型
  5. 从下游传播错误? → 使用
    %w
    包装并添加上下文

2. Sentinel Errors

2. 哨兵错误

Use package-level
var
for errors that callers need to check:
go
// ✅ 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
Callers check with
errors.Is
:
go
if errors.Is(err, user.ErrNotFound) {
    // handle not found
}
NEVER compare errors with
==
. Always use
errors.Is()
.
使用包级
var
变量定义供调用方检测的错误:
go
// ✅ 规范做法 — 导出的哨兵错误
var (
    ErrNotFound     = errors.New("user: not found")
    ErrUnauthorized = errors.New("user: unauthorized")
)

// 命名约定:Err + 描述信息
// 消息中添加包上下文前缀
调用方使用
errors.Is
进行检测:
go
if errors.Is(err, user.ErrNotFound) {
    // 处理未找到场景
}
切勿使用
==
比较错误。始终使用
errors.Is()

3. Custom Error Types

3. 自定义错误类型

When errors need to carry structured information:
go
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 with errors.As:
var valErr *ValidationError
if errors.As(err, &valErr) {
    log.Printf("invalid field: %s", valErr.Field)
}
当错误需要携带结构化信息时:
go
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation: field %s: %s", e.Field, e.Message)
}

// 调用方使用errors.As提取:
var valErr *ValidationError
if errors.As(err, &valErr) {
    log.Printf("invalid field: %s", valErr.Field)
}

4. Error Wrapping

4. 错误包装

ALWAYS add context when propagating errors up the stack. Use
%w
to preserve the error chain:
go
// ✅ 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
保留错误链:
go
// ✅ 规范做法 — 添加上下文,保留错误链
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)
    }
    // ...
}

// ❌ 错误做法 — 无上下文
return nil, err

// ❌ 错误做法 — 错误链中断,调用方无法使用errors.Is/As
return nil, fmt.Errorf("failed: %v", err)

When NOT to use
%w

何时不使用
%w

Use
%v
instead of
%w
when you explicitly want to break the error chain, preventing callers from depending on internal implementation errors:
go
// Intentionally hiding internal DB error from public API
return nil, fmt.Errorf("user lookup failed: %v", err)
当你明确想要中断错误链,防止调用方依赖内部实现错误时,请使用
%v
而非
%w
go
// 有意向公共API隐藏内部数据库错误
return nil, fmt.Errorf("user lookup failed: %v", err)

5. Handle Errors Exactly Once

5. 错误仅处理一次

An error should be either logged OR returned, never both:
go
// ✅ 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
    }
    // ...
}
The rule: the component that decides what to do about the error is the one that logs/metrics it. Everyone else wraps and returns.
一个错误要么被记录日志,要么被返回,切勿两者皆做:
go
// ✅ 规范做法 — 返回错误,由调用方决定处理方式
func loadConfig(path string) (*Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("load config %s: %w", path, err)
    }
    // ...
}

// ❌ 错误做法 — 既记录日志又返回(错误被处理两次)
func loadConfig(path string) (*Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        log.Printf("failed to read config: %v", err) // 第一次处理
        return nil, err                                 // 第二次处理
    }
    // ...
}
规则:决定如何处理错误的组件负责记录日志/上报指标,其他组件只需包装并返回错误。

6. Error Naming Conventions

6. 错误命名约定

go
// 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)
go
// 哨兵错误:以Err为前缀
var ErrNotFound = errors.New("not found")

// 错误类型:以Error为后缀
type NotFoundError struct { ... }
type ValidationError struct { ... }

// 错误消息:小写,无标点,无"failed to"前缀
// 包含上下文:"包名: 操作: 详情"
errors.New("auth: token expired")
fmt.Errorf("user: get by id %d: %w", id, err)

7. Panic Rules

7. Panic使用规则

Panic is NOT error handling. Use panic only when:
  • Program initialization fails and cannot continue (
    template.Must
    , flag parsing)
  • Programmer error that should never happen (violated invariant)
  • Nil dereference that indicates a bug, not a runtime condition
In tests, use
t.Fatal
/
t.FailNow
, never
panic
.
In HTTP handlers and middleware, recover from panics at the boundary to prevent one request from crashing the server.
Panic并非错误处理方式。仅在以下场景使用panic:
  • 程序初始化失败且无法继续运行(如
    template.Must
    、标志解析)
  • 程序员错误,这种情况本不应发生(违反不变量)
  • 空指针解引用,表明存在bug而非运行时条件
在测试中,请使用
t.Fatal
/
t.FailNow
,切勿使用
panic
在HTTP处理器和中间件中,需在边界处捕获panic,以防止单个请求导致服务器崩溃。

8. Error Checking Patterns

8. 错误检查模式

go
// Inline error check — preferred for simple cases
if err := doSomething(); err != nil {
    return fmt.Errorf("do something: %w", err)
}

// Multi-return with named result — acceptable for complex functions
func process() (result string, err error) {
    defer func() {
        if err != nil {
            err = fmt.Errorf("process: %w", err)
        }
    }()
    // ...
}

// errors.Join for multiple errors (Go 1.20+)
var errs []error
for _, item := range items {
    if err := validate(item); err != nil {
        errs = append(errs, err)
    }
}
return errors.Join(errs...)
go
// 内联错误检查 — 简单场景首选
if err := doSomething(); err != nil {
    return fmt.Errorf("do something: %w", err)
}

// 带命名返回值的多返回 — 复杂函数可接受
func process() (result string, err error) {
    defer func() {
        if err != nil {
            err = fmt.Errorf("process: %w", err)
        }
    }()
    // ...
}

// errors.Join用于多错误处理(Go 1.20+)
var errs []error
for _, item := range items {
    if err := validate(item); err != nil {
        errs = append(errs, err)
    }
}
return errors.Join(errs...)

Verification Checklist

验证检查清单

  1. No
    _
    discarding errors (unless explicitly justified with comment)
  2. Every
    fmt.Errorf
    wrapping uses
    %w
    (or
    %v
    with documented reason)
  3. Sentinel errors use
    var Err...
    naming
  4. Custom error types implement
    error
    interface
  5. Callers use
    errors.Is
    /
    errors.As
    , never
    ==
    or type assertion
  6. No log-and-return patterns
  7. Error messages are lowercase, contextual, chain-friendly
  1. _
    丢弃错误的情况(除非有明确注释说明理由)
  2. 所有用于包装的
    fmt.Errorf
    均使用
    %w
    (或使用
    %v
    且有文档说明理由)
  3. 哨兵错误使用
    var Err...
    命名方式
  4. 自定义错误类型实现
    error
    接口
  5. 调用方使用
    errors.Is
    /
    errors.As
    ,切勿使用
    ==
    或类型断言
  6. 无“记录并返回”的模式
  7. 错误消息为小写、带上下文且利于错误链处理