mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2343 字
6 分钟
Go 错误处理最佳实践
2022-08-09

一、Go 错误哲学:errors are values#

1.1 为什么 Go 选择「返回错误」而非「异常」?#

Go 的设计者们在语言诞生之初就做出了一个关键决策:不使用异常机制,而是通过多返回值让错误成为一等公民。这个设计源于几个核心考量:

  1. 显式优于隐式:异常的控制流是隐式的,throwcatch 可能在代码任何位置发生跳转,而 Go 的错误处理要求开发者显式检查每个可能失败的操作。

  2. 可预测的控制流:阅读 Go 代码时,你可以清晰地看到每个函数调用后的错误处理逻辑,不存在「隐藏」的控制流转移。

  3. 性能考量:异常机制在异常触发时需要栈展开(stack unwinding),而 Go 的错误返回几乎零开销。

Rob Pike 在「Errors are values」一文中强调:

错误就是值,它们可以像其他值一样被程序化地处理。

这意味着错误不是「特殊情况」,而是程序逻辑的常态部分。

1.2 Go 与其他语言错误处理对比#

flowchart TB subgraph Go["Go 错误处理"] G1["函数返回 (result, error)"] G2["调用方显式检查 error"] G3["处理或向上传递"] G1 --> G2 --> G3 end subgraph Exception["异常机制 (Java/Python)"] E1["抛出异常 throw"] E2["栈展开寻找 catch"] E3["捕获并处理"] E1 --> E2 --> E3 end subgraph Result["Result 类型 (Rust)"] R1["返回 Result<T, E>"] R2["模式匹配处理"] R3["Ok/Err 分支"] R1 --> R2 --> R3 end G3 --> G4["优点: 显式、可预测<br/>缺点: 代码冗余"] E3 --> E4["优点: 代码简洁<br/>缺点: 隐式控制流"] R3 --> R4["优点: 类型安全<br/>缺点: 学习曲线"] style Go fill:#6bcb77 style Exception fill:#ffd93d style Result fill:#4d96ff

1.3 error 接口的本质#

Go 的 error 类型极其简单,仅包含一个方法:

type error interface {
Error() string
}

这个极简设计带来几个重要特性:

  • 零开销:大多数情况下,error 只是一个指针
  • 可扩展:任何实现了 Error() string 的类型都是 error
  • 不可变语义Error() 返回字符串,暗示错误信息应该是不可变的

源码中,最常用的实现是 errorString

// src/errors/errors.go — https://github.com/golang/go/blob/go1.25.0/src/errors/errors.go
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}

二、error 接口与自定义错误#

2.1 标准库的 errors.New#

当你只需要一个简单的错误时,errors.New 是最直接的选择:

func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}

errors.New 会返回一个指向 errorString 的指针。注意这里返回指针而非值,是为了避免在比较时出现意外——两个内容相同但地址不同的 errorString 不应该被视为相等。

2.2 fmt.Errorf:格式化错误信息#

当需要动态构造错误信息时,fmt.Errorf 是更好的选择:

func openConfig(path string) (*os.File, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("failed to open config file %q: %w", path, err)
}
return f, nil
}

fmt.Errorf 支持格式化动词,其中 %w(wrap)是 Go 1.13 引入的重要特性,它将错误包装起来,保留原始错误的信息链。

2.3 自定义错误类型#

当需要携带更多上下文或实现特定行为时,可以自定义错误类型:

// 业务错误码
type ErrorCode int
const (
ErrNotFound ErrorCode = iota + 1
ErrUnauthorized
ErrInvalidInput
)
// 自定义错误类型
type BusinessError struct {
Code ErrorCode
Message string
Cause error // 原始错误
}
func (e *BusinessError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Cause)
}
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
// 实现 Unwrap 方法,支持 errors.Is/As
func (e *BusinessError) Unwrap() error {
return e.Cause
}
// 工厂函数
func NewNotFoundError(resource string, cause error) error {
return &BusinessError{
Code: ErrNotFound,
Message: fmt.Sprintf("%s not found", resource),
Cause: cause,
}
}

2.4 自定义错误的最佳实践#

// 好的做法:提供有意义的错误信息
type ValidationError struct {
Field string
Value any
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on field %q: %s (got %v)",
e.Field, e.Message, e.Value)
}
// 提供类型断言方法(避免导出内部字段)
func IsValidationError(err error) bool {
var ve *ValidationError
return errors.As(err, &ve)
}
// 实现 Is 方法,支持自定义相等语义
func (e *ValidationError) Is(target error) bool {
t, ok := target.(*ValidationError)
if !ok {
return false
}
return e.Field == t.Field
}

三、错误包装(Error Wrapping)#

3.1 为什么需要错误包装?#

在调用链中,原始错误往往缺乏上下文。比如一个「连接数据库失败」的错误,在服务层可能需要知道:

  • 连接的是哪个数据库?
  • 发生在哪个业务流程中?
  • 具体的底层错误是什么?

错误包装通过在原始错误外层添加上下文,形成一条错误链,同时保留原始错误以便溯源。

3.2 fmt.Errorf 与 %w 动词#

Go 1.13 引入了 %w 格式化动词,用于错误包装:

func processOrder(orderID string) error {
order, err := fetchOrder(orderID)
if err != nil {
return fmt.Errorf("failed to process order %s: %w", orderID, err)
}
// ...
return nil
}
func fetchOrder(orderID string) (*Order, error) {
row := db.QueryRow("SELECT * FROM orders WHERE id = ?", orderID)
var order Order
if err := row.Scan(&order); err != nil {
return fmt.Errorf("failed to fetch order: %w", err)
}
return &order, nil
}

错误链的结构如下:

flowchart BT subgraph 链尾["原始错误"] E1["sql.ErrNoRows"] end subgraph 中间层["Repository 层包装"] E2["failed to fetch order: %w"] end subgraph 链头["Service 层包装"] E3["failed to process order xxx: %w"] end E1 -->|"Unwrap"| E2 E2 -->|"Unwrap"| E3 style E1 fill:#ff6b6b style E2 fill:#ffd93d style E3 fill:#6bcb77

错误链遍历原理

sequenceDiagram participant Code as 调用代码 participant Is as errors.Is participant Chain as 错误链 participant Unwrap as Unwrap方法 Code->>Is: errors.Is(err, sql.ErrNoRows) Is->>Chain: 获取当前错误 loop 遍历错误链 Chain->>Is: 返回当前错误 Is->>Is: 比较是否等于目标错误 alt 匹配成功 Is->>Code: 返回 true else 不匹配 Is->>Unwrap: 调用 Unwrap() Unwrap->>Chain: 返回下一个错误 end end Is->>Code: 返回 false

3.3 errors.Is 与 errors.As:遍历错误链#

Go 1.13 同时引入了 errors.Iserrors.As,用于在错误链中查找特定错误:

// errors.Is:检查错误链中是否包含特定错误值
if errors.Is(err, sql.ErrNoRows) {
// 处理"记录不存在"的情况
}
// errors.As:提取错误链中特定类型的错误
var berr *BusinessError
if errors.As(err, &berr) {
// 根据 ErrorCode 进行不同处理
switch berr.Code {
case ErrNotFound:
// ...
case ErrUnauthorized:
// ...
}
}

重要区别

  • errors.Is 用于比较(哨兵错误)
  • errors.As 用于提取类型

3.4 errors.Join:多错误合并(Go 1.20)#

Go 1.20 引入了 errors.Join,用于合并多个错误:

func validateUser(user *User) error {
var errs []error
if user.Name == "" {
errs = append(errs, errors.New("name is required"))
}
if user.Email == "" {
errs = append(errs, errors.New("email is required"))
}
if user.Age < 0 {
errs = append(errs, errors.New("age must be positive"))
}
return errors.Join(errs...)
}
// 使用
if err := validateUser(user); err != nil {
// err.Error() 会返回所有错误的组合信息
fmt.Println(err)
// Output: name is required
// email is required
// 注意:errors.Is 比较的是错误值(指针),不是错误消息
// 必须使用哨兵错误变量,而非 errors.New() 直接构造
// 因为 errors.New() 每次返回不同的指针,errors.Is 永远为 false
var ErrNameRequired = errors.New("name is required")
if errors.Is(err, ErrNameRequired) {
// ...
}
}

errors.Join 返回一个实现了 Unwrap() []error 方法的错误,errors.Iserrors.As 都能正确处理它。


四、哨兵错误(Sentinel Errors)#

4.1 什么是哨兵错误?#

哨兵错误是预定义的错误值,用于表示特定的错误条件:

// 标准库中的哨兵错误
var (
ErrUnsupported = errors.New("unsupported operation")
ErrNotExist = errors.New("does not exist")
ErrExist = errors.New("already exists")
)

这些错误通常在包级别定义,命名以 Err 开头。

4.2 哨兵错误的使用场景#

// 定义
var (
ErrUserNotFound = errors.New("user not found")
ErrUserDisabled = errors.New("user is disabled")
ErrInvalidPassword = errors.New("invalid password")
)
// 使用
func authenticate(username, password string) error {
user, err := getUser(username)
if err != nil {
return fmt.Errorf("authentication failed: %w", err)
}
if user.Status == "disabled" {
return fmt.Errorf("authentication failed: %w", ErrUserDisabled)
}
if !verifyPassword(user.PasswordHash, password) {
return fmt.Errorf("authentication failed: %w", ErrInvalidPassword)
}
return nil
}
// 调用方
err := authenticate("alice", "password123")
if errors.Is(err, ErrUserNotFound) {
// 用户不存在
} else if errors.Is(err, ErrUserDisabled) {
// 用户被禁用
} else if errors.Is(err, ErrInvalidPassword) {
// 密码错误
}

4.3 哨兵错误的局限性#

  1. 缺乏上下文:哨兵错误无法携带动态信息
  2. 包耦合:使用方需要导入定义哨兵错误的包
  3. 全局状态:可能被意外修改(虽然 Go 不鼓励这种做法)

4.4 哨兵错误 vs 自定义错误类型#

特性哨兵错误自定义错误类型
上下文丰富
类型检查errors.Iserrors.As
定义方式包级变量结构体
使用场景简单错误条件需要额外信息

五、错误链与错误溯源#

5.1 理解错误链结构#

错误链是由多层包装形成的树状结构:

type wrappedError struct {
msg string
err error
}
func (e *wrappedError) Error() string {
return e.msg + ": " + e.err.Error()
}
func (e *wrappedError) Unwrap() error {
return e.err
}

错误链的数据结构

flowchart BT subgraph 原始错误["最底层:原始错误"] E1["sql.ErrNoRows<br/>(哨兵错误)"] end subgraph 第二层["第二层:Repository 包装"] E2["wrappedError<br/>msg: 'failed to fetch order'<br/>err: E1"] end subgraph 第三层["第三层:Service 包装"] E3["wrappedError<br/>msg: 'failed to process order xxx'<br/>err: E2"] end subgraph 最外层["最外层:Handler 处理"] E4["统一错误响应<br/>记录日志"] end E1 -->|"Unwrap"| E2 E2 -->|"Unwrap"| E3 E3 -->|"传递"| E4 style E1 fill:#ff6b6b style E2 fill:#ffd93d style E3 fill:#4d96ff style E4 fill:#6bcb77

5.2 errors.Unwrap:手动遍历#

func printErrorChain(err error) {
for err != nil {
fmt.Println("-", err.Error())
err = errors.Unwrap(err)
}
}

5.3 实现自定义的 Unwrap#

type DetailedError struct {
Op string // 操作名称
Path string // 相关路径
Err error // 原始错误
}
func (e *DetailedError) Error() string {
return fmt.Sprintf("%s %s: %v", e.Op, e.Path, e.Err)
}
func (e *DetailedError) Unwrap() error {
return e.Err
}

5.4 错误溯源的最佳实践#

// 每一层只添加最相关的上下文
func handleRequest(w http.ResponseWriter, r *http.Request) {
data, err := fetchUserData(r.Context(), userID)
if err != nil {
// 只添加请求级别的上下文
err = fmt.Errorf("handling request for user %s: %w", userID, err)
handleError(w, err)
return
}
// ...
}
func fetchUserData(ctx context.Context, userID string) (*UserData, error) {
user, err := db.GetUser(ctx, userID)
if err != nil {
// 只添加数据访问级别的上下文
return nil, fmt.Errorf("fetching user from database: %w", err)
}
// ...
}
// 避免在每一层都重复相同的上下文
func badExample() error {
err := doSomething()
if err != nil {
// 这里的"user foo"和下面的"user foo"重复了
return fmt.Errorf("processing user foo: %w", err)
}
// ...
}

错误处理模式决策图:

flowchart TD A["收到错误 err"] --> B{"能处理这个错误吗?"} B -- "能处理" --> C{"需要返回新错误?"} C -- "是" --> D["处理并返回新错误"] C -- "否" --> E["处理并返回 nil"] B -- "不能处理" --> F{"需要添加上下文?"} F -- "是" --> G["fmt.Errorf('context: %w', err)"] F -- "否" --> H["直接返回 err"] G --> I["调用方处理"] H --> I I --> J{"是否为最外层?"} J -- "是" --> K["记录日志并响应"] J -- "否" --> B style D fill:#6bcb77 style E fill:#6bcb77 style G fill:#ffd93d style H fill:#4d96ff style K fill:#ff6b6b

错误传播与处理层次图

flowchart TB subgraph Handler["Handler 层"] H1["接收请求"] H2["验证参数"] H3["响应结果"] H4["统一错误处理<br/>记录日志"] end subgraph Service["Service 层"] S1["业务逻辑"] S2["组合多个 Repository"] S3["添加业务上下文"] end subgraph Repository["Repository 层"] R1["数据库操作"] R2["外部服务调用"] R3["添加数据访问上下文"] end H1 --> H2 --> S1 S1 --> S2 --> R1 R1 -->|"数据库错误"| R3 R3 -->|"包装后"| S3 S3 -->|"包装后"| H4 H4 --> H3 style H4 fill:#ff6b6b style S3 fill:#ffd93d style R3 fill:#4d96ff

六、defer + recover 机制#

6.1 panic 与 recover 的本质#

Go 没有异常,但有 panicrecover。它们的设计目的与异常不同:

  • panic:不可恢复的错误,如程序逻辑错误、不可继续的状态
  • recover:在 defer 中捕获 panic,用于清理或转换错误
func example() (err error) {
defer func() {
if r := recover(); r != nil {
// 将 panic 转换为 error
err = fmt.Errorf("panic recovered: %v", r)
// 打印堆栈以便调试
debug.PrintStack()
}
}()
// 可能 panic 的代码
riskyOperation()
return nil
}

panic/recover 执行流程:

sequenceDiagram participant G as goroutine participant F1 as 函数 A participant F2 as 函数 B participant Defer as defer 链 participant Runtime as runtime G->>F1: 调用函数 A F1->>F2: 调用函数 B F2->>Runtime: panic("error") Runtime->>F2: 开始栈展开 Runtime->>Defer: 执行 B 的 defer Runtime->>F1: 继续栈展开 Runtime->>Defer: 执行 A 的 defer alt defer 中有 recover Defer->>Runtime: recover() Runtime->>F1: 恢复执行 F1->>G: 返回错误 else 无 recover Runtime->>G: goroutine 退出 Runtime->>Runtime: 打印堆栈 end

6.2 合理使用 panic/recover#

应该使用 panic 的场景

// 1. 不可能的条件(程序错误)
func MustCompile(pattern string) *Regexp {
re, err := Compile(pattern)
if err != nil {
panic(`regexp: Compile(` + quote(pattern) + `): ` + err.Error())
}
return re
}
// 2. 初始化失败
func init() {
if os.Getenv("REQUIRED_VAR") == "" {
panic("REQUIRED_VAR environment variable is required")
}
}

应该使用 recover 的场景

// 1. HTTP 服务器的错误隔离
func middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("panic recovered: %v", err)
http.Error(w, "Internal Server Error", 500)
}
}()
next.ServeHTTP(w, r)
})
}
// 2. goroutine 隔离
func safeGo(fn func() error) <-chan error {
errCh := make(chan error, 1)
go func() {
defer func() {
if r := recover(); r != nil {
errCh <- fmt.Errorf("panic: %v", r)
}
}()
errCh <- fn()
}()
return errCh
}

6.3 不应该用 panic/recover 的场景#

// 不要用 panic 做常规错误处理
func badExample(path string) error {
if !fileExists(path) {
panic("file not found") // 错误!应该返回 error
}
return nil
}
// 正确做法
func goodExample(path string) error {
if !fileExists(path) {
return os.ErrNotExist
}
return nil
}

七、错误日志记录策略#

7.1 在哪里记录错误?#

一个常见的问题是:应该在错误发生处记录,还是在处理处记录?

原则:错误只记录一次,在「处理」层记录

// 错误示范:每一层都记录
func service() error {
if err := repo(); err != nil {
log.Printf("service error: %v", err) // 第一次记录
return err
}
return nil
}
func repo() error {
if err := db(); err != nil {
log.Printf("repo error: %v", err) // 第二次记录
return err
}
return nil
}
// 正确做法:只在最外层记录
func handler(w http.ResponseWriter, r *http.Request) {
if err := service(); err != nil {
log.Printf("request failed: %v", err) // 唯一记录点
http.Error(w, "internal error", 500)
}
}
func service() error {
return repo() // 只传递,不记录
}

7.2 结构化日志#

使用结构化日志记录更多上下文:

import "log/slog"
func processOrder(ctx context.Context, orderID string) error {
order, err := fetchOrder(ctx, orderID)
if err != nil {
// 包含完整的上下文信息
slog.Error("failed to process order",
"order_id", orderID,
"error", err,
"trace_id", traceIDFromContext(ctx),
)
return err
}
return nil
}

7.3 错误码与可观测性#

type ErrorWithCode struct {
Code string
Message string
Err error
}
func (e *ErrorWithCode) Error() string {
return e.Message
}
func (e *ErrorWithCode) Unwrap() error {
return e.Err
}
// 记录时提取错误码用于监控
func logError(err error) {
var e *ErrorWithCode
code := "UNKNOWN"
if errors.As(err, &e) {
code = e.Code
}
metrics.Counter("errors").WithLabelValues(code).Inc()
slog.Error("error occurred", "code", code, "error", err)
}

八、第三方错误库#

8.1 pkg/errors#

在 Go 1.13 之前,github.com/pkg/errors 是最流行的错误处理库:

import "github.com/pkg/errors"
func main() {
err := readFile("config.json")
if err != nil {
// 打印带堆栈的错误
fmt.Printf("%+v\n", err)
}
}
func readFile(path string) error {
_, err := os.ReadFile(path)
if err != nil {
// 自动捕获调用堆栈
return errors.Wrap(err, "failed to read config")
}
return nil
}

主要功能:

  • errors.Wrap:包装错误并捕获堆栈
  • errors.WithStack:仅捕获堆栈不添加消息
  • errors.Cause:获取根本原因

8.2 Go 1.20+ 标准库能力#

Go 1.20 之后,标准库已经足够强大:

// 错误包装
err = fmt.Errorf("context: %w", err)
// 多错误合并
err = errors.Join(err1, err2, err3)
// 错误检查
errors.Is(err, target)
errors.As(err, &target)
// 遍历错误链
errors.Unwrap(err)

8.3 如何选择?#

场景推荐
新项目,Go 1.20+标准库足够
需要堆栈信息pkg/errorsgo-errors/errors
需要丰富的错误码自定义错误类型
多错误聚合标准库 errors.Join

九、防御性编程与错误处理#

9.1 输入验证#

func CreateUser(name, email string, age int) (*User, error) {
// 在操作前验证所有输入
if name == "" {
return nil, &ValidationError{
Field: "name",
Message: "name is required",
}
}
if !isValidEmail(email) {
return nil, &ValidationError{
Field: "email",
Message: "invalid email format",
}
}
if age < 0 || age > 150 {
return nil, &ValidationError{
Field: "age",
Message: "age must be between 0 and 150",
}
}
// 通过验证,执行创建
return &User{Name: name, Email: email, Age: age}, nil
}

9.2 错误处理模式#

// 模式 1:立即返回
func pattern1() error {
if err := step1(); err != nil {
return err
}
if err := step2(); err != nil {
return err
}
return nil
}
// 模式 2:延迟清理
func pattern2(path string) (err error) {
f, err := os.Create(path)
if err != nil {
return err
}
defer func() {
if cerr := f.Close(); cerr != nil && err == nil {
err = cerr
}
}()
// 使用 f...
return nil
}
// 模式 3:错误分组(Go 1.20+)
func pattern3(ctx context.Context) error {
var g errgroup.Group
g.Go(func() error { return task1(ctx) })
g.Go(func() error { return task2(ctx) })
return g.Wait()
}

9.3 资源清理#

func processFile(path string) error {
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("open file: %w", err)
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
// 处理每一行
if err := processLine(scanner.Text()); err != nil {
// 即使出错,defer 也会确保文件关闭
return fmt.Errorf("process line: %w", err)
}
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("scan file: %w", err)
}
return nil
}

十、实战案例:构建 Web 服务的错误处理#

10.1 分层错误处理#

// 错误定义(internal/errors/errors.go)
type AppError struct {
Code string
HTTPStatus int
Message string
Err error
}
func (e *AppError) Error() string {
return e.Message
}
func (e *AppError) Unwrap() error {
return e.Err
}
// 预定义错误
var (
ErrNotFound = &AppError{
Code: "NOT_FOUND",
HTTPStatus: http.StatusNotFound,
Message: "resource not found",
}
ErrUnauthorized = &AppError{
Code: "UNAUTHORIZED",
HTTPStatus: http.StatusUnauthorized,
Message: "unauthorized",
}
)
// Repository 层
func (r *userRepository) GetByID(ctx context.Context, id string) (*User, error) {
var user User
err := r.db.GetContext(ctx, &user, "SELECT * FROM users WHERE id = ?", id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("user %s: %w", id, ErrNotFound)
}
return nil, fmt.Errorf("query user: %w", err)
}
return &user, nil
}
// Service 层
func (s *userService) GetProfile(ctx context.Context, userID string) (*Profile, error) {
user, err := s.repo.GetByID(ctx, userID)
if err != nil {
return nil, err // 直接传递,不重复包装
}
// 业务逻辑...
return &Profile{User: user}, nil
}
// Handler 层
func (h *handler) GetProfile(w http.ResponseWriter, r *http.Request) {
userID := chi.URLParam(r, "userID")
profile, err := h.service.GetProfile(r.Context(), userID)
if err != nil {
respondWithError(w, err)
return
}
respondJSON(w, http.StatusOK, profile)
}
// 统一错误响应
func respondWithError(w http.ResponseWriter, err error) {
var appErr *AppError
if errors.As(err, &appErr) {
respondJSON(w, appErr.HTTPStatus, ErrorResponse{
Code: appErr.Code,
Message: appErr.Message,
})
return
}
// 未知错误,返回 500
slog.Error("internal error", "error", err)
respondJSON(w, http.StatusInternalServerError, ErrorResponse{
Code: "INTERNAL_ERROR",
Message: "an internal error occurred",
})
}

10.2 错误处理流程图#

flowchart TD A[Handler 接收请求] --> B[验证输入] B -->|验证失败| C[返回 400 错误] B -->|验证通过| D[调用 Service] D --> E{Service 处理} E -->|业务错误| F[返回业务错误码] E -->|调用 Repository| G{Repository 处理} G -->|数据库错误| H[包装为 AppError] G -->|成功| I[返回数据] H --> F F --> J[Handler 统一响应] I --> J C --> J

十一、常见错误模式与反模式#

11.1 反模式:吞掉错误#

// 错误:忽略错误
func bad() {
_, _ = os.Open("file.txt") // 忽略错误
}
// 错误:记录但不返回
func alsoBad() error {
if err := doSomething(); err != nil {
log.Printf("error: %v", err) // 只记录不返回
// 继续执行...
}
return nil
}
// 正确:处理并返回
func good() error {
if err := doSomething(); err != nil {
return fmt.Errorf("do something: %w", err)
}
return nil
}

11.2 反模式:过度包装#

// 每一层都添加相似的信息
func bad() error {
if err := service(); err != nil {
return fmt.Errorf("failed to process order: %w", err)
}
return nil
}
func service() error {
if err := repo(); err != nil {
return fmt.Errorf("failed to process order: %w", err) // 重复!
}
return nil
}

11.3 反模式:使用 panic 做流程控制#

// 错误
func validate(input string) {
if input == "" {
panic("input is empty")
}
}
// 正确
func validate(input string) error {
if input == "" {
return errors.New("input is empty")
}
return nil
}

11.4 良好模式总结#

// 1. 错误只传递不处理,在最外层统一处理
// 2. 每层只添加自己层级的上下文
// 3. 使用 errors.Is/As 而非 == 比较
// 4. 结构化日志,包含 trace_id 等上下文
// 5. 区分可恢复错误和不可恢复错误

总结:Go 错误处理的核心原则#

12.1 核心原则#

  1. 显式处理:每个错误都应该被显式处理或显式传递
  2. 错误是值:可以编程处理,可以存储,可以比较
  3. 只记录一次:在最外层统一记录,避免日志泛滥
  4. 保留上下文:使用 %w 包装错误,形成可追溯的错误链
  5. 区分层次:不同层次使用不同的错误表示方式

12.2 决策树#

需要处理错误?
├── 能处理 → 处理并返回 nil 或新错误
├── 需要添加上下文 → fmt.Errorf("context: %w", err)
└── 不能处理 → 直接返回 err

12.3 工具箱#

功能标准库第三方
创建错误errors.New-
格式化错误fmt.Errorf-
错误包装fmt.Errorf + %wpkg/errors.Wrap
错误比较errors.Is-
类型断言errors.As-
多错误errors.Joinhashicorp/go-multierror
堆栈捕获-pkg/errors.WithStack

Go 的错误处理虽然需要更多代码,但它带来的可预测性和可维护性是值得的。掌握这些最佳实践,能让你的 Go 代码更加健壮和专业。

十二、常见问题#

Q1:errors.Is 和 == 比较错误有什么区别?#

errors.Is 会遍历错误链(通过 Unwrap),可以匹配被包装的底层错误。== 只比较顶层错误值。哨兵错误必须用 errors.Is 检查。

Q2:什么时候该用 panic 而不是 error?#

panic 用于不可恢复的程序错误(如索引越界、空指针解引用)。error 用于可恢复的预期错误(如文件不存在、网络超时)。库代码不应 panic,除非在 Must 前缀的函数中。

Q3:fmt.Errorf 的 %w 和 %v 有什么区别?#

%w 包装错误(实现 Unwrap 方法),允许 errors.Is/As 遍历错误链。%v 只是格式化错误消息,不保留错误链。需要错误链时用 %w

Q4:Go 1.20 的 errors.Join 有什么用?#

errors.Join 合并多个错误为一个,实现了 Unwrap() []error。适合验证场景:收集所有错误后一次性返回,而不是遇到第一个错误就返回。

小结#

  • Go 错误哲学:errors are values,通过多返回值显式处理错误
  • Go 1.13 引入 %w 包装 + errors.Is/As 遍历错误链,是现代 Go 错误处理的基石
  • 哨兵错误用于特定错误条件,自定义错误类型用于携带上下文
  • 错误只记录一次,在最外层统一处理,避免日志泛滥
  • defer + recover 用于不可恢复错误的隔离,不应替代常规错误处理

参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

Go 错误处理最佳实践
https://blog.souloss.com/posts/golang/go-error/
作者
Souloss
发布于
2022-08-09
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时