go-error-handling
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseGo 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:
- Simple, no extra context needed? →
errors.New("message") - Need to add context to existing error? →
fmt.Errorf("doing X: %w", err) - Caller needs to detect this error? → Sentinel or custom type
var - Error carries structured data? → Custom type implementing
error - Propagating from downstream? → Wrap with and add context
%w
当创建或返回错误时,请遵循以下决策流程:
- 简单场景,无需额外上下文? →
errors.New("message") - 需要为现有错误添加上下文? →
fmt.Errorf("doing X: %w", err) - 调用方需要检测该错误? → 哨兵变量或自定义类型
var - 错误需要携带结构化数据? → 实现接口的自定义类型
error - 从下游传播错误? → 使用包装并添加上下文
%w
2. Sentinel Errors
2. 哨兵错误
Use package-level for errors that callers need to check:
vargo
// ✅ 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 messageCallers check with :
errors.Isgo
if errors.Is(err, user.ErrNotFound) {
// handle not found
}NEVER compare errors with . Always use .
==errors.Is()使用包级变量定义供调用方检测的错误:
vargo
// ✅ 规范做法 — 导出的哨兵错误
var (
ErrNotFound = errors.New("user: not found")
ErrUnauthorized = errors.New("user: unauthorized")
)
// 命名约定:Err + 描述信息
// 消息中添加包上下文前缀调用方使用进行检测:
errors.Isgo
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 to preserve the error chain:
%wgo
// ✅ 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)在向上传播错误时,务必添加上下文。
使用保留错误链:
%wgo
// ✅ 规范做法 — 添加上下文,保留错误链
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何时不使用%w
%wUse instead of when you explicitly want to break the error chain,
preventing callers from depending on internal implementation errors:
%v%wgo
// Intentionally hiding internal DB error from public API
return nil, fmt.Errorf("user lookup failed: %v", err)当你明确想要中断错误链,防止调用方依赖内部实现错误时,请使用而非:
%v%wgo
// 有意向公共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 (, flag parsing)
template.Must - Programmer error that should never happen (violated invariant)
- Nil dereference that indicates a bug, not a runtime condition
In tests, use / , never .
t.Fatalt.FailNowpanicIn HTTP handlers and middleware, recover from panics at the boundary
to prevent one request from crashing the server.
Panic并非错误处理方式。仅在以下场景使用panic:
- 程序初始化失败且无法继续运行(如、标志解析)
template.Must - 程序员错误,这种情况本不应发生(违反不变量)
- 空指针解引用,表明存在bug而非运行时条件
在测试中,请使用/,切勿使用。
t.Fatalt.FailNowpanic在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
验证检查清单
- No discarding errors (unless explicitly justified with comment)
_ - Every wrapping uses
fmt.Errorf(or%wwith documented reason)%v - Sentinel errors use naming
var Err... - Custom error types implement interface
error - Callers use /
errors.Is, nevererrors.Asor type assertion== - No log-and-return patterns
- Error messages are lowercase, contextual, chain-friendly
- 无丢弃错误的情况(除非有明确注释说明理由)
_ - 所有用于包装的均使用
fmt.Errorf(或使用%w且有文档说明理由)%v - 哨兵错误使用命名方式
var Err... - 自定义错误类型实现接口
error - 调用方使用/
errors.Is,切勿使用errors.As或类型断言== - 无“记录并返回”的模式
- 错误消息为小写、带上下文且利于错误链处理