404 Not Found

404 Not Found


nginx

错误处理

第10课:错误处理

生活类比

想象你去餐厅点餐。服务员告诉你:"抱歉,这道菜今天卖完了。"——这是一个可预期的情况,你可以换一道菜,生活继续。这就是 Go 中的 error

但如果厨房突然着火了,所有人必须立刻撤离——这是一个不可预期的灾难。这就是 Go 中的 panic

Go 的错误处理哲学很朴素:能预料的问题就优雅地处理,不能预料的问题才让它崩溃。 不像其他语言用 try/catch 把所有异常混在一起,Go 要求你显式地、逐个地处理每一个错误。


核心概念

概念 说明
error 接口 Go 内置的错误类型,只有一个 Error() string 方法
多返回值 Go 函数通常将错误作为最后一个返回值
errors.New 创建简单的文本错误
fmt.Errorf 创建格式化的错误(可配合 %w 包装)
errors.Is 判断错误链中是否包含特定错误
errors.As 从错误链中提取特定类型的错误
panic 触发运行时恐慌,程序崩溃
recover defer 中捕获 panic,阻止程序崩溃
错误包装 %w 或自定义类型包装底层错误,保留上下文

错误处理流程图

函数返回 error
       │
       ▼
  err != nil ?
  ┌────┴────┐
  │ Yes     │ No
  ▼         ▼
处理错误    继续执行
  │
  ├─ 可恢复 → 记录日志 / 返回默认值 / 重试
  ├─ 需上报 → 包装后向上传递
  └─ 不可恢复 → panic

基本语法与用法

1. error 接口

error 是 Go 内置的接口类型,定义非常简洁:

GO
// error 接口的定义(内置,无需导入)
type error interface {
    Error() string
}

任何实现了 Error() string 方法的类型都是一个 error

2. 创建错误

GO
package main

import (
    "errors"
    "fmt"
)

func main() {
    // 方式一:errors.New —— 创建简单文本错误
    err1 := errors.New("文件不存在")

    // 方式二:fmt.Errorf —— 创建格式化错误
    filename := "config.yaml"
    err2 := fmt.Errorf("读取文件 %s 失败", filename)

    // 方式三:fmt.Errorf + %w —— 包装底层错误(推荐)
    baseErr := errors.New("permission denied")
    err3 := fmt.Errorf("无法写入日志: %w", baseErr)

    fmt.Println(err1) // 文件不存在
    fmt.Println(err2) // 读取文件 config.yaml 失败
    fmt.Println(err3) // 无法写入日志: permission denied
}

3. 检查错误

GO
package main

import (
    "errors"
    "fmt"
)

var ErrNotFound = errors.New("记录未找到")

func findUser(id int) (string, error) {
    if id <= 0 {
        return "", ErrNotFound
    }
    return "张三", nil
}

func main() {
    name, err := findUser(0)
    if err != nil {
        // errors.Is 判断错误链中是否包含目标错误
        if errors.Is(err, ErrNotFound) {
            fmt.Println("用户不存在,请检查ID")
        } else {
            fmt.Println("未知错误:", err)
        }
        return
    }
    fmt.Println("找到用户:", name)
}

4. 提取特定类型的错误

GO
package main

import (
    "errors"
    "fmt"
)

// 自定义错误类型
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("验证失败 [%s]: %s", e.Field, e.Message)
}

func validateAge(age int) error {
    if age < 0 || age > 150 {
        return &ValidationError{
            Field:   "age",
            Message: "年龄必须在 0-150 之间",
        }
    }
    return nil
}

func main() {
    err := validateAge(200)
    if err != nil {
        // errors.As 从错误链中提取特定类型
        var ve *ValidationError
        if errors.As(err, &ve) {
            fmt.Printf("字段: %s, 原因: %s\n", ve.Field, ve.Message)
        } else {
            fmt.Println("其他错误:", err)
        }
    }
}

5. panic 和 recover

GO
package main

import "fmt"

// safeDivide 使用 recover 捕获 panic
func safeDivide(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("捕获到恐慌: %v", r)
        }
    }()

    // 除以零会触发 panic
    return a / b, nil
}

func main() {
    result, err := safeDivide(10, 0)
    if err != nil {
        fmt.Println("错误:", err) // 错误: 捕获到恐慌: runtime error: integer divide by zero
        return
    }
    fmt.Println("结果:", result)
}
💡 提示: Go 中绝大多数错误应该通过返回 error 处理,而不是 panic。只在程序确实无法继续运行时才使用 panic(如初始化失败、不可恢复的逻辑错误)。

💡 提示: 使用 fmt.Errorf 包装错误时,始终用 %w 而非 %v%w 保留了原始错误,使得 errors.Iserrors.As 能正确工作。

💡 提示: 错误信息应该是小写字母开头、不以标点结尾的陈述句,因为它们经常被拼接在更大的错误消息中。


示例

示例:基本错误处理(难度⭐)

模拟一个简单的配置文件读取器,演示最基本的错误返回和检查模式。

GO
package main

import (
    "errors"
    "fmt"
    "os"
)

// 定义包级别的哨兵错误
var (
    ErrFileNotFound  = errors.New("配置文件未找到")
    ErrFileEmpty     = errors.New("配置文件为空")
    ErrInvalidFormat = errors.New("配置文件格式无效")
)

// Config 表示应用配置
type Config struct {
    Host string
    Port int
}

// LoadConfig 从文件加载配置(模拟)
func LoadConfig(path string) (*Config, error) {
    // 模拟文件不存在的情况
    if path == "" {
        return nil, ErrFileNotFound
    }

    // 模拟读取文件
    data, err := os.ReadFile(path)
    if err != nil {
        // 包装底层错误,保留原始信息
        return nil, fmt.Errorf("读取配置文件 %s: %w", path, err)
    }

    // 检查文件是否为空
    if len(data) == 0 {
        return nil, ErrFileEmpty
    }

    // 模拟解析配置
    return &Config{
        Host: "localhost",
        Port: 8080,
    }, nil
}

func main() {
    // 尝试加载配置
    config, err := LoadConfig("app.conf")
    if err != nil {
        // 使用 errors.Is 判断具体错误类型
        switch {
        case errors.Is(err, ErrFileNotFound):
            fmt.Println("错误: 请指定配置文件路径")
        case errors.Is(err, ErrFileEmpty):
            fmt.Println("错误: 配置文件为空,请检查内容")
        case errors.Is(err, ErrInvalidFormat):
            fmt.Println("错误: 配置文件格式不正确")
        default:
            fmt.Println("错误:", err)
        }
        return
    }

    fmt.Printf("配置加载成功: %s:%d\n", config.Host, config.Port)
}
▶ 试一试

运行结果(当文件不存在时):

错误: 请指定配置文件路径

示例:错误包装与链式判断(难度⭐⭐)

演示多层函数调用中错误的包装、传播和链式判断。

GO
package main

import (
    "errors"
    "fmt"
)

// 定义业务错误
var (
    ErrUserNotFound = errors.New("用户不存在")
    ErrPermission   = errors.New("权限不足")
)

// 表示数据库层错误
type DatabaseError struct {
    Operation string
    Table     string
    Err       error
}

func (e *DatabaseError) Error() string {
    return fmt.Sprintf("数据库错误 [%s.%s]: %e", e.Table, e.Operation, e.Err)
}

func (e *DatabaseError) Unwrap() error {
    return e.Err
}

// UserRepository 用户仓库
type UserRepository struct {
    users map[int]string
}

// FindByID 根据ID查找用户
func (r *UserRepository) FindByID(id int) (string, error) {
    name, exists := r.users[id]
    if !exists {
        // 包装为数据库层错误
        return "", &DatabaseError{
            Operation: "SELECT",
            Table:     "users",
            Err:       ErrUserNotFound,
        }
    }
    return name, nil
}

// Service 业务服务层
type Service struct {
    repo *UserRepository
}

// GetUser 获取用户信息
func (s *Service) GetUser(id int, requireAdmin bool) (string, error) {
    name, err := s.repo.FindByID(id)
    if err != nil {
        // 向上包装,添加业务上下文
        return "", fmt.Errorf("获取用户信息(id=%d): %w", id, err)
    }

    if requireAdmin && name != "admin" {
        return "", fmt.Errorf("用户 %s: %w", name, ErrPermission)
    }

    return name, nil
}

func main() {
    repo := &UserRepository{
        users: map[int]string{
            1: "admin",
            2: "张三",
        },
    }
    svc := &Service{repo: repo}

    // 测试场景
    testCases := []struct {
        name        string
        id          int
        requireAdmin bool
    }{
        {"查找admin", 1, false},
        {"查找普通用户", 2, false},
        {"查找不存在的用户", 99, false},
        {"普通用户访问管理功能", 2, true},
    }

    for _, tc := range testCases {
        fmt.Printf("--- %s ---\n", tc.name)
        user, err := svc.GetUser(tc.id, tc.requireAdmin)
        if err != nil {
            fmt.Printf("  失败: %v\n", err)

            // errors.Is 可以穿透错误链找到根因
            if errors.Is(err, ErrUserNotFound) {
                fmt.Println("  处理: 提示用户检查ID")
            } else if errors.Is(err, ErrPermission) {
                fmt.Println("  处理: 提示用户联系管理员")
            }

            // errors.As 可以提取中间层的特定错误类型
            var dbErr *DatabaseError
            if errors.As(err, &dbErr) {
                fmt.Printf("  数据库详情: 表=%s, 操作=%s\n", dbErr.Table, dbErr.Operation)
            }
        } else {
            fmt.Printf("  成功: 用户 %s\n", user)
        }
        fmt.Println()
    }
}
▶ 试一试

运行结果:

--- 查找admin ---
  成功: 用户 admin

--- 查找普通用户 ---
  成功: 用户 张三

--- 查找不存在的用户 ---
  失败: 获取用户信息(id=99): 数据库错误 [users.SELECT]: 用户不存在
  处理: 提示用户检查ID
  数据库详情: 表=users, 操作=SELECT

--- 普通用户访问管理功能 ---
  失败: 用户 张三: 权限不足
  处理: 提示用户联系管理员

示例:自定义错误类型与 recover 实战(难度⭐⭐⭐)

实现一个完整的 HTTP 请求处理器,包含自定义错误类型、错误码、panic 恢复中间件。

GO
package main

import (
    "errors"
    "fmt"
    "runtime/debug"
)

// ==================== 自定义错误体系 ====================

// AppError 应用级错误,包含错误码和上下文
type AppError struct {
    Code    int               // 业务错误码
    Message string            // 用户友好的消息
    Detail  string            // 开发者调试信息
    Cause   error             // 底层原因
    Context map[string]string // 附加上下文
}

func (e *AppError) 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)
}

func (e *AppError) Unwrap() error {
    return e.Cause
}

// Is 实现自定义的错误比较逻辑
func (e *AppError) Is(target error) bool {
    t, ok := target.(*AppError)
    if !ok {
        return false
    }
    return e.Code == t.Code
}

// NewAppError 创建新的应用错误
func NewAppError(code int, message string) *AppError {
    return &AppError{
        Code:    code,
        Message: message,
        Context: make(map[string]string),
    }
}

// WithCause 设置底层原因
func (e *AppError) WithCause(err error) *AppError {
    e.Cause = err
    return e
}

// WithDetail 设置调试详情
func (e *AppError) WithDetail(detail string) *AppError {
    e.Detail = detail
    return e
}

// WithContext 添加上下文信息
func (e *AppError) WithContext(key, value string) *AppError {
    e.Context[key] = value
    return e
}

// 预定义错误码
var (
    ErrBadRequest   = NewAppError(400, "请求参数错误")
    ErrUnauthorized = NewAppError(401, "未授权访问")
    ErrForbidden    = NewAppError(403, "禁止访问")
    ErrNotFound     = NewAppError(404, "资源不存在")
    ErrInternal     = NewAppError(500, "服务器内部错误")
)

// ==================== 模拟业务层 ====================

// Request 模拟 HTTP 请求
type Request struct {
    UserID   int
    Path     string
    IsAdmin  bool
}

// Response 模拟 HTTP 响应
type Response struct {
    StatusCode int
    Body       string
}

// validateRequest 验证请求参数
func validateRequest(req *Request) error {
    if req.UserID <= 0 {
        return ErrBadRequest.
            WithDetail("user_id 必须为正整数").
            WithContext("user_id", fmt.Sprintf("%d", req.UserID))
    }
    if req.Path == "" {
        return ErrBadRequest.
            WithDetail("path 不能为空")
    }
    return nil
}

// authenticate 认证
func authenticate(req *Request) error {
    if req.UserID == 0 {
        return ErrUnauthorized.WithDetail("缺少认证凭据")
    }
    return nil
}

// authorize 授权
func authorize(req *Request) error {
    if !req.IsAdmin {
        return ErrForbidden.
            WithDetail("需要管理员权限").
            WithContext("user_id", fmt.Sprintf("%d", req.UserID))
    }
    return nil
}

// findResource 查找资源(可能触发 panic)
func findResource(path string) (string, error) {
    // 模拟一个会 panic 的 bug
    if path == "/crash" {
        var p *int
        *p = 1 // 空指针解引用,触发 panic
    }

    resources := map[string]string{
        "/users":   "用户列表",
        "/profile": "个人资料",
    }

    data, exists := resources[path]
    if !exists {
        return "", ErrNotFound.
            WithDetail(fmt.Sprintf("路径 %s 对应的资源不存在", path))
    }
    return data, nil
}

// handleRequest 处理请求(业务逻辑)
func handleRequest(req *Request) (*Response, error) {
    // 1. 验证参数
    if err := validateRequest(req); err != nil {
        return nil, err
    }

    // 2. 认证
    if err := authenticate(req); err != nil {
        return nil, err
    }

    // 3. 授权(仅管理路径需要)
    if req.Path == "/admin" {
        if err := authorize(req); err != nil {
            return nil, err
        }
    }

    // 4. 查找资源
    data, err := findResource(req.Path)
    if err != nil {
        return nil, err
    }

    return &Response{
        StatusCode: 200,
        Body:       data,
    }, nil
}

// ==================== Recovery 中间件 ====================

// RecoveryMiddleware panic 恢复中间件
func RecoveryMiddleware(handler func(*Request) (*Response, error)) func(*Request) (*Response, error) {
    return func(req *Request) (resp *Response, err error) {
        defer func() {
            if r := recover(); r != nil {
                // 记录 panic 堆栈信息
                stack := string(debug.Stack())
                fmt.Printf("[PANIC] %v\n堆栈:\n%s\n", r, stack)

                // 返回内部错误,而不是让程序崩溃
                err = ErrInternal.
                    WithDetail(fmt.Sprintf("panic: %v", r)).
                    WithCause(fmt.Errorf("panic: %v", r))
            }
        }()

        return handler(req)
    }
}

// ==================== 错误格式化 ====================

// formatError 将错误格式化为友好的响应
func formatError(err error) *Response {
    var appErr *AppError
    if errors.As(err, &appErr) {
        msg := fmt.Sprintf("错误 [%d]: %s", appErr.Code, appErr.Message)
        if appErr.Detail != "" {
            msg += fmt.Sprintf("\n  详情: %s", appErr.Detail)
        }
        if len(appErr.Context) > 0 {
            msg += "\n  上下文:"
            for k, v := range appErr.Context {
                msg += fmt.Sprintf("\n    %s: %s", k, v)
            }
        }
        return &Response{
            StatusCode: appErr.Code,
            Body:       msg,
        }
    }

    return &Response{
        StatusCode: 500,
        Body:       fmt.Sprintf("未知错误: %v", err),
    }
}

// ==================== 主函数 ====================

func main() {
    // 用 Recovery 中间件包装处理器
    safeHandler := RecoveryMiddleware(handleRequest)

    // 测试各种场景
    testCases := []struct {
        name string
        req  *Request
    }{
        {
            name: "正常请求",
            req:  &Request{UserID: 1, Path: "/users", IsAdmin: true},
        },
        {
            name: "无效参数",
            req:  &Request{UserID: -1, Path: "/users"},
        },
        {
            name: "未认证",
            req:  &Request{UserID: 0, Path: "/users"},
        },
        {
            name: "权限不足",
            req:  &Request{UserID: 2, Path: "/admin", IsAdmin: false},
        },
        {
            name: "资源不存在",
            req:  &Request{UserID: 1, Path: "/unknown"},
        },
        {
            name: "触发 panic(自动恢复)",
            req:  &Request{UserID: 1, Path: "/crash"},
        },
    }

    for _, tc := range testCases {
        fmt.Printf("=== %s ===\n", tc.name)

        resp, err := safeHandler(tc.req)
        if err != nil {
            resp = formatError(err)
        }

        fmt.Printf("  状态码: %d\n", resp.StatusCode)
        fmt.Printf("  响应:   %s\n\n", resp.Body)
    }
}
▶ 试一试

运行结果:

=== 正常请求 ===
  状态码: 200
  响应:   用户列表

=== 无效参数 ===
  状态码: 400
  响应:   错误 [400]: 请求参数错误
  详情: user_id 必须为正整数
  上下文:
    user_id: -1

=== 未认证 ===
  状态码: 401
  响应:   错误 [401]: 未授权访问
  详情: 缺少认证凭据

=== 权限不足 ===
  状态码: 403
  响应:   错误 [403]: 禁止访问
  详情: 需要管理员权限
  上下文:
    user_id: 2

=== 资源不存在 ===
  状态码: 404
  响应:   错误 [404]: 资源不存在
  详情: 路径 /unknown 对应的资源不存在

=== 触发 panic(自动恢复) ===
[PANIC] runtime error: invalid memory address or nil pointer dereference
堆栈:
...
  状态码: 500
  响应:   错误 [500]: 服务器内部错误
  详情: panic: runtime error: invalid memory address or nil pointer dereference

实际场景

场景一:数据库事务回滚

在涉及多个步骤的数据库操作中,任何一步失败都需要回滚已执行的操作。

GO
package main

import (
    "errors"
    "fmt"
)

var (
    ErrBalanceInsufficient = errors.New("余额不足")
    ErrAccountFrozen       = errors.New("账户已冻结")
)

// TxError 事务错误,记录失败步骤
type TxError struct {
    Step    string
    Cause   error
    Actions []string // 已执行的操作,需要回滚
}

func (e *TxError) Error() string {
    return fmt.Sprintf("事务失败 [步骤: %s]: %v", e.Step, e.Cause)
}

func (e *TxError) Unwrap() error {
    return e.Cause
}

// TransferService 转账服务
type TransferService struct {
    balances map[string]float64
    frozen   map[string]bool
}

// Transfer 执行转账事务
func (s *TransferService) Transfer(from, to string, amount float64) error {
    var executedSteps []string

    // 步骤1:验证源账户
    if s.frozen[from] {
        return &TxError{
            Step:  "验证源账户",
            Cause: ErrAccountFrozen,
        }
    }
    executedSteps = append(executedSteps, "锁定源账户")

    // 步骤2:检查余额
    if s.balances[from] < amount {
        return &TxError{
            Step:    "检查余额",
            Cause:   ErrBalanceInsufficient,
            Actions: executedSteps,
        }
    }
    executedSteps = append(executedSteps, "检查余额通过")

    // 步骤3:扣减源账户
    s.balances[from] -= amount
    executedSteps = append(executedSteps, fmt.Sprintf("扣减 %.2f 元", amount))

    // 步骤4:验证目标账户
    if s.frozen[to] {
        return &TxError{
            Step:    "验证目标账户",
            Cause:   ErrAccountFrozen,
            Actions: executedSteps, // 此时需要回滚已扣减的金额
        }
    }

    // 步骤5:增加目标账户
    s.balances[to] += amount

    fmt.Printf("  转账成功: %s -> %s, 金额: %.2f\n", from, to, amount)
    return nil
}

func main() {
    svc := &TransferService{
        balances: map[string]float64{
            "张三": 1000,
            "李四": 500,
            "王五": 0,
        },
        frozen: map[string]bool{
            "王五": true,
        },
    }

    tests := []struct {
        name string
        from string
        to   string
        amt  float64
    }{
        {"正常转账", "张三", "李四", 200},
        {"余额不足", "张三", "李四", 9999},
        {"目标账户冻结", "张三", "王五", 100},
    }

    for _, t := range tests {
        fmt.Printf("--- %s ---\n", t.name)
        fmt.Printf("  转账前: %s=%.2f, %s=%.2f\n",
            t.from, svc.balances[t.from], t.to, svc.balances[t.to])

        err := svc.Transfer(t.from, t.to, t.amt)
        if err != nil {
            fmt.Printf("  失败: %v\n", err)

            // 检查是否需要回滚
            var txErr *TxError
            if errors.As(err, &txErr) && len(txErr.Actions) > 0 {
                fmt.Printf("  回滚 %d 个已执行操作:\n", len(txErr.Actions))
                for _, action := range txErr.Actions {
                    fmt.Printf("    - 撤销: %s\n", action)
                }
                // 实际项目中这里执行回滚逻辑
                // 例如: svc.balances[from] += amount
            }
        }

        fmt.Printf("  转账后: %s=%.2f, %s=%.2f\n\n",
            t.from, svc.balances[t.from], t.to, svc.balances[t.to])
    }
}

场景二:文件批量处理与错误收集

处理多个文件时,不应因一个文件失败而中断全部操作,而是收集所有错误后统一报告。

GO
package main

import (
    "errors"
    "fmt"
    "strings"
)

var (
    ErrFileCorrupted = errors.New("文件已损坏")
    ErrPermission    = errors.New("权限不足")
    ErrDiskFull      = errors.New("磁盘空间不足")
)

// FileError 单个文件的错误
type FileError struct {
    Path  string
    Op    string
    Cause error
}

func (e *FileError) Error() string {
    return fmt.Sprintf("[%s] %s: %v", e.Op, e.Path, e.Cause)
}

func (e *FileError) Unwrap() error {
    return e.Cause
}

// BatchResult 批量处理结果
type BatchResult struct {
    Success []string
    Failed  []FileError
}

func (r *BatchResult) HasErrors() bool {
    return len(r.Failed) > 0
}

func (r *BatchResult) Summary() string {
    var sb strings.Builder
    sb.WriteString(fmt.Sprintf("处理完成: 成功 %d, 失败 %d\n",
        len(r.Success), len(r.Failed)))

    if len(r.Success) > 0 {
        sb.WriteString("成功文件:\n")
        for _, f := range r.Success {
            sb.WriteString(fmt.Sprintf("  ✓ %s\n", f))
        }
    }

    if len(r.Failed) > 0 {
        sb.WriteString("失败文件:\n")
        for _, e := range r.Failed {
            sb.WriteString(fmt.Sprintf("  ✗ %s (%s: %v)\n", e.Path, e.Op, e.Cause))
        }
    }

    return sb.String()
}

// processFile 模拟处理单个文件
func processFile(path string) error {
    // 模拟各种可能的错误
    fileErrors := map[string]error{
        "corrupted.txt": ErrFileCorrupted,
        "secret.txt":    ErrPermission,
        "huge.txt":      ErrDiskFull,
    }

    if err, exists := fileErrors[path]; exists {
        return &FileError{
            Path:  path,
            Op:    "处理",
            Cause: err,
        }
    }
    return nil
}

// processFiles 批量处理文件
func processFiles(paths []string) *BatchResult {
    result := &BatchResult{}

    for _, path := range paths {
        err := processFile(path)
        if err != nil {
            result.Failed = append(result.Failed, FileError{
                Path:  path,
                Op:    "处理",
                Cause: errors.Unwrap(err),
            })
        } else {
            result.Success = append(result.Success, path)
        }
    }

    return result
}

func main() {
    files := []string{
        "report.pdf",
        "data.csv",
        "corrupted.txt",
        "image.png",
        "secret.txt",
        "huge.txt",
        "readme.md",
    }

    fmt.Printf("开始处理 %d 个文件...\n\n", files[files])

    result := processFiles(files)
    fmt.Println(result.Summary())

    // 根据错误类型给出不同建议
    for _, fe := range result.Failed {
        switch {
        case errors.Is(fe.Cause, ErrFileCorrupted):
            fmt.Printf("建议: %s 需要从备份恢复\n", fe.Path)
        case errors.Is(fe.Cause, ErrPermission):
            fmt.Printf("建议: 请检查 %s 的文件权限\n", fe.Path)
        case errors.Is(fe.Cause, ErrDiskFull):
            fmt.Printf("建议: 清理磁盘空间后重试 %s\n", fe.Path)
        }
    }
}

❓ 常见问题

Q1: 什么时候该用 panic,什么时候该返回 error?

原则:error 处理可预期的失败,用 panic 处理"不应该发生"的编程错误。

GO
// ✓ 正确:返回 error —— 用户输入不可预期
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("除数不能为零")
    }
    return a / b, nil
}

// ✓ 正确:用 panic —— 这是程序员的 bug,不应该传入 nil
func MustNotNil(v interface{}) {
    if v == nil {
        panic("值不应为 nil,这是调用方的编程错误")
    }
}

实际规则:

Q2: errors.Is 和 errors.As 有什么区别?

GO
// errors.Is: 判断错误链中是否包含某个特定的哨兵错误
// 相当于: err == target || err.Unwrap() == target || ...
errors.Is(err, ErrNotFound)  // true/false

// errors.As: 从错误链中提取某个特定类型的错误
// 相当于类型断言,但会遍历整个错误链
var appErr *AppError
errors.As(err, &appErr)  // true + 赋值, 或 false

// 类比:
// errors.Is  → "你是不是张三?"(判断身份)
// errors.As  → "你是不是程序员?如果是,告诉我你的技能"(提取信息)

Q3: 为什么错误信息建议小写开头、不加标点?

Go 官方和社区的惯例:

GO
// ✓ 推荐
fmt.Errorf("读取文件 %s 失败: %w", name, err)
// 输出: 读取文件 config.yaml 失败: permission denied

// ✗ 不推荐
fmt.Errorf("读取文件 %s 失败。: %w", name, err)  // 多余的标点
fmt.Errorf("读取文件 %s 失败!: %w", name, err)  // 多余的感叹号
fmt.Errorf("Read file %s failed: %w", name, err)  // 不统一的语言

原因是错误经常被拼接在一起形成错误链,小写无标点的格式更容易阅读:

初始化数据库: 连接 postgres: dial tcp 127.0.0.1:5432: connection refused

Q4: 如何比较被包装过的错误?

直接用 == 无法比较被包装的错误,必须用 errors.Is

GO
var ErrSentinel = errors.New("sentinel")

wrapped := fmt.Errorf("context: %w", ErrSentinel)

// ✗ 错误:直接比较会失败
fmt.Println(wrapped == ErrSentinel)  // false

// ✓ 正确:使用 errors.Is
fmt.Println(errors.Is(wrapped, ErrSentinel))  // true

// errors.Is 会遍历整个错误链:
// wrapped → Unwrap() → ErrSentinel → 匹配!

📖 小节

主题 要点
error 接口 只需实现 Error() string 方法
创建错误 errors.New 创建简单错误,fmt.Errorf + %w 创建包装错误
检查错误 errors.Is 判断类型,errors.As 提取类型
自定义错误 实现 Error() + Unwrap() 方法,包含业务上下文
panic/recover 仅用于不可恢复的错误;在 defer 中用 recover 捕获
错误哲学 显式处理、逐个检查、错误是值不是异常
错误包装 %w 保留底层错误,用 %v 丢弃底层错误
社区惯例 错误信息小写无标点,作为函数最后返回值

Go 错误处理的三条黄金法则:

  1. 立即检查 —— 拿到 error 后马上判断 err != nil
  2. 逐层包装 —— 每层添加上下文信息,用 %w 保留原始错误
  3. 优雅降级 —— 能恢复就恢复,不能恢复就向上报告,最后才 panic

📝 作业

练习1:实现一个带重试的文件读取器

编写一个函数 ReadWithRetry(path string, maxRetries int) ([]byte, error),功能要求:

GO
// 参考框架
var ErrMaxRetriesExceeded = errors.New("达到最大重试次数")

func ReadWithRetry(path string, maxRetries int) ([]byte, error) {
    var lastErr error
    for i := 0; i < maxRetries; i++ {
        data, err := os.ReadFile(path)
        if err == nil {
            return data, nil
        }
        lastErr = err
        fmt.Printf("  第 %d 次重试失败: %v\n", i+1, err)
    }
    return nil, fmt.Errorf("读取 %s: %w (最后错误: %v)", path, ErrMaxRetriesExceeded, lastErr)
}

练习2:构建错误等级系统

实现一个错误等级系统,支持以下错误级别:

GO
type Level int

const (
    LevelDebug Level = iota
    LevelInfo
    LevelWarn
    LevelError
    LevelFatal
)

type LeveledError struct {
    Level   Level
    Message string
    Cause   error
}

要求:

练习3:实现错误收集器(并发安全)

编写一个 ErrorCollector 类型,用于并发场景中收集多个 goroutine 的错误:

GO
type ErrorCollector struct {
    // 你需要的字段
}

// Add 添加一个错误(并发安全)
func (c *ErrorCollector) Add(err error) { ... }

// Errors 返回所有收集到的错误
func (c *ErrorCollector) Errors() []error { ... }

// HasErrors 是否有错误
func (c *ErrorCollector) HasErrors() bool { ... }

// Error 将所有错误合并为一个 error
func (c *ErrorCollector) Error() error { ... }

要求:


下一课

完成本课后,请继续学习 第11课:包与模块,了解如何组织 Go 代码、管理依赖、以及发布自己的包。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏