编程基础 0005_错误处理进阶

Go 错误处理进阶

目录

  1. Go 错误处理哲学
  2. error 接口本质
  3. 自定义错误类型
  4. fmt.Errorf 与 %w 包装错误
  5. errors.Is 和 errors.As
  6. 哨兵错误模式
  7. 错误处理最佳实践
  8. 实际项目中的错误处理模式

1. Go 错误处理哲学

1.1 与 try-catch 的根本区别

在 Java、Python、C++ 等语言中,异常处理依赖 try-catch 机制——异常被"抛出"后沿调用栈向上传播,直到被某个 catch 块捕获。这种模式有几个问题:

  • 异常的传播路径是隐式的,调用者不知道某个函数可能抛出什么异常
  • 异常控制流类似 goto,会打断正常的代码逻辑
  • 容易被滥用(用异常做流程控制)

Go 采用了完全不同的哲学:错误是值(errors are values)。错误通过函数返回值显式传递,调用者必须主动检查并处理错误。

// Java 风格(隐式异常传播)
try {
    File f = openFile("config.json");
    String data = readAll(f);
    Config config = parse(data);
} catch (FileNotFoundException e) {
    // 处理文件不存在
} catch (IOException e) {
    // 处理IO错误
} catch (ParseException e) {
    // 处理解析错误
}

// Go 风格(显式错误处理)
f, err := os.Open("config.json")
if err != nil {
    return fmt.Errorf("打开配置文件失败: %w", err)
}
defer f.Close()

data, err := io.ReadAll(f)
if err != nil {
    return fmt.Errorf("读取配置文件失败: %w", err)
}

var config Config
if err := json.Unmarshal(data, &config); err != nil {
    return fmt.Errorf("解析配置文件失败: %w", err)
}

1.2 显式错误处理的优势

package main

import (
    "fmt"
    "strconv"
)

// 每个可能失败的操作都显式返回错误
// 调用者可以清楚看到哪些步骤可能出错
func parseAndDouble(s string) (int, error) {
    // 第一步:解析字符串为整数
    n, err := strconv.Atoi(s)
    if err != nil {
        return 0, fmt.Errorf("解析整数失败: %w", err)
    }

    // 第二步:检查范围
    if n < 0 || n > 1000 {
        return 0, fmt.Errorf("数值 %d 超出有效范围 [0, 1000]", n)
    }

    return n * 2, nil
}

func main() {
    // 正常情况
    result, err := parseAndDouble("42")
    if err != nil {
        fmt.Println("错误:", err)
        return
    }
    fmt.Println("结果:", result) // 结果: 84

    // 非数字
    _, err = parseAndDouble("abc")
    if err != nil {
        fmt.Println("错误:", err) // 错误: 解析整数失败: strconv.Atoi: parsing "abc": invalid syntax
    }

    // 超出范围
    _, err = parseAndDouble("9999")
    if err != nil {
        fmt.Println("错误:", err) // 错误: 数值 9999 超出有效范围 [0, 1000]
    }
}

1.3 Go 的错误处理格言

Rob Pike 在 "Errors are values" 博文中指出,既然错误是普通的值,就可以用编程技巧来简化错误处理,而不是机械地 if err != nil

package main

import (
    "bufio"
    "fmt"
    "os"
)

// errWriter 封装了 writer 和错误状态
// 一旦发生错误,后续写入操作自动跳过
type errWriter struct {
    w   *bufio.Writer
    err error
}

func (ew *errWriter) write(buf []byte) {
    if ew.err != nil {
        return // 已经有错误了,跳过
    }
    _, ew.err = ew.w.Write(buf)
}

func (ew *errWriter) flush() error {
    if ew.err != nil {
        return ew.err
    }
    return ew.w.Flush()
}

func writeReport(f *os.File) error {
    bw := bufio.NewWriter(f)
    ew := &errWriter{w: bw}

    // 不需要每次都检查错误,最后统一检查
    ew.write([]byte("=== 报告标题 ===\n"))
    ew.write([]byte("第一部分内容...\n"))
    ew.write([]byte("第二部分内容...\n"))
    ew.write([]byte("=== 报告结束 ===\n"))

    return ew.flush()
}

func main() {
    f, err := os.Create("/tmp/report.txt")
    if err != nil {
        fmt.Println("创建文件失败:", err)
        return
    }
    defer f.Close()

    if err := writeReport(f); err != nil {
        fmt.Println("写入报告失败:", err)
        return
    }
    fmt.Println("报告写入成功")
}

2. error 接口本质

2.1 error 是一个接口

Go 内置的 error 类型实际上是一个非常简单的接口:

// Go 标准库中的定义
type error interface {
    Error() string
}

任何实现了 Error() string 方法的类型都满足 error 接口。这种设计极其简洁而强大。

2.2 标准库中的 errors.New 和 fmt.Errorf

package main

import (
    "errors"
    "fmt"
)

func main() {
    // errors.New 创建一个简单的错误
    err1 := errors.New("something went wrong")
    fmt.Println(err1)        // something went wrong
    fmt.Printf("%T\n", err1) // *errors.errorString

    // fmt.Errorf 创建格式化的错误
    name := "config.json"
    err2 := fmt.Errorf("无法打开文件 %s", name)
    fmt.Println(err2) // 无法打开文件 config.json

    // error 的零值是 nil,表示没有错误
    var err3 error
    fmt.Println(err3 == nil) // true
}

2.3 errors.New 的内部实现

// 标准库 errors 包的实现非常简单
// 这里展示其核心逻辑

package main

import "fmt"

// errorString 是一个实现了 error 接口的简单结构体
type errorString struct {
    s string
}

// Error 方法使 errorString 满足 error 接口
func (e *errorString) Error() string {
    return e.s
}

// New 创建一个新的错误,返回指针
// 返回指针而不是值,是为了让每次 New 调用产生不同的错误实例
func New(text string) error {
    return &errorString{text}
}

func main() {
    err1 := New("出错了")
    err2 := New("出错了")

    // 虽然文本相同,但它们是不同的错误实例(指针不同)
    fmt.Println(err1 == err2) // false

    fmt.Println(err1) // 出错了
}

2.4 nil error 的陷阱

package main

import "fmt"

// MyError 自定义错误类型
type MyError struct {
    Code    int
    Message string
}

func (e *MyError) Error() string {
    return fmt.Sprintf("错误码 %d: %s", e.Code, e.Message)
}

func doSomething(fail bool) error {
    // 注意:这里声明了一个具体类型的指针
    var err *MyError

    if fail {
        err = &MyError{Code: 404, Message: "未找到"}
    }

    // 危险!即使 err 是 nil 指针,返回后 error 接口值不为 nil
    return err
}

func doSomethingCorrect(fail bool) error {
    if fail {
        return &MyError{Code: 404, Message: "未找到"}
    }
    // 直接返回 nil,不经过具体类型变量
    return nil
}

func main() {
    // 错误示范:即使没有失败,err 也不等于 nil!
    err := doSomething(false)
    if err != nil {
        // 会进入这里!因为 error 接口内部持有 (*MyError, nil)
        // 接口值只有在类型和值都为 nil 时才等于 nil
        fmt.Println("错误的判断 - 进入了错误分支")
        fmt.Printf("err 类型: %T, 值: %v\n", err, err)
    }

    // 正确示范
    err2 := doSomethingCorrect(false)
    if err2 == nil {
        fmt.Println("正确:没有错误") // 正确:没有错误
    }
}

关键教训:永远不要返回一个具体类型的 nil 指针作为 error 接口值。如果没有错误,直接 return nil


3. 自定义错误类型

3.1 简单的自定义错误

package main

import "fmt"

// ValidationError 验证错误,包含字段名和错误原因
type ValidationError struct {
    Field   string
    Message string
}

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

// NotFoundError 资源未找到错误
type NotFoundError struct {
    Resource string
    ID       string
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s (ID: %s) 不存在", e.Resource, e.ID)
}

func validateAge(age int) error {
    if age < 0 {
        return &ValidationError{
            Field:   "age",
            Message: "年龄不能为负数",
        }
    }
    if age > 150 {
        return &ValidationError{
            Field:   "age",
            Message: "年龄不能超过150",
        }
    }
    return nil
}

func findUser(id string) (string, error) {
    // 模拟查找用户
    users := map[string]string{
        "1": "Alice",
        "2": "Bob",
    }
    name, ok := users[id]
    if !ok {
        return "", &NotFoundError{
            Resource: "用户",
            ID:       id,
        }
    }
    return name, nil
}

func main() {
    // 测试验证错误
    if err := validateAge(-5); err != nil {
        fmt.Println(err) // 验证失败 [age]: 年龄不能为负数

        // 类型断言获取详细信息
        if ve, ok := err.(*ValidationError); ok {
            fmt.Printf("字段: %s, 原因: %s\n", ve.Field, ve.Message)
        }
    }

    // 测试未找到错误
    _, err := findUser("99")
    if err != nil {
        fmt.Println(err) // 用户 (ID: 99) 不存在

        if nfe, ok := err.(*NotFoundError); ok {
            fmt.Printf("资源类型: %s, ID: %s\n", nfe.Resource, nfe.ID)
        }
    }
}

3.2 携带上下文的错误类型

package main

import (
    "fmt"
    "time"
)

// OpError 记录操作上下文的错误
type OpError struct {
    Op      string    // 操作名称
    Path    string    // 资源路径
    Err     error     // 底层错误(支持错误链)
    Time    time.Time // 发生时间
}

func (e *OpError) Error() string {
    return fmt.Sprintf("操作 %s 在 %s 上失败 [%s]: %v",
        e.Op, e.Path, e.Time.Format("15:04:05"), e.Err)
}

// Unwrap 支持错误链解包
func (e *OpError) Unwrap() error {
    return e.Err
}

// PermissionError 权限错误
type PermissionError struct {
    User   string
    Action string
}

func (e *PermissionError) Error() string {
    return fmt.Sprintf("用户 %s 没有 %s 权限", e.User, e.Action)
}

func readConfig(user string) error {
    // 模拟权限检查失败
    permErr := &PermissionError{
        User:   user,
        Action: "read",
    }

    // 包装为操作错误
    return &OpError{
        Op:   "ReadConfig",
        Path: "/etc/app/config.yaml",
        Err:  permErr,
        Time: time.Now(),
    }
}

func main() {
    err := readConfig("guest")
    if err != nil {
        fmt.Println(err)
        // 操作 ReadConfig 在 /etc/app/config.yaml 上失败 [14:30:05]: 用户 guest 没有 read 权限
    }
}

3.3 多错误聚合

package main

import (
    "fmt"
    "strings"
)

// MultiError 聚合多个错误
type MultiError struct {
    Errors []error
}

func (me *MultiError) Error() string {
    if len(me.Errors) == 0 {
        return "没有错误"
    }
    msgs := make([]string, len(me.Errors))
    for i, err := range me.Errors {
        msgs[i] = fmt.Sprintf("  [%d] %s", i+1, err.Error())
    }
    return fmt.Sprintf("发生 %d 个错误:\n%s", len(me.Errors), strings.Join(msgs, "\n"))
}

// Add 添加错误(忽略 nil)
func (me *MultiError) Add(err error) {
    if err != nil {
        me.Errors = append(me.Errors, err)
    }
}

// ErrorOrNil 如果没有错误则返回 nil
func (me *MultiError) ErrorOrNil() error {
    if len(me.Errors) == 0 {
        return nil
    }
    return me
}

// validateUser 同时验证多个字段
func validateUser(name string, age int, email string) error {
    me := &MultiError{}

    if name == "" {
        me.Add(fmt.Errorf("姓名不能为空"))
    }
    if age < 0 || age > 150 {
        me.Add(fmt.Errorf("年龄 %d 不在有效范围内", age))
    }
    if !strings.Contains(email, "@") {
        me.Add(fmt.Errorf("邮箱 %q 格式不正确", email))
    }

    return me.ErrorOrNil()
}

func main() {
    // 多个字段验证失败
    err := validateUser("", -1, "not-an-email")
    if err != nil {
        fmt.Println(err)
        // 发生 3 个错误:
        //   [1] 姓名不能为空
        //   [2] 年龄 -1 不在有效范围内
        //   [3] 邮箱 "not-an-email" 格式不正确
    }

    // 全部验证通过
    err = validateUser("Alice", 30, "alice@example.com")
    fmt.Println("全部通过:", err == nil) // 全部通过: true
}

4. fmt.Errorf 与 %w 包装错误

4.1 错误包装基础(Go 1.13+)

Go 1.13 引入了错误包装机制,通过 fmt.Errorf%w 动词可以将一个错误包装在另一个错误中,形成错误链。

package main

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

func readFile(path string) ([]byte, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        // %w 包装原始错误,保留错误链
        return nil, fmt.Errorf("读取文件 %s: %w", path, err)
    }
    return data, nil
}

func loadConfig() ([]byte, error) {
    data, err := readFile("/etc/app/config.json")
    if err != nil {
        // 继续包装,形成多层错误链
        return nil, fmt.Errorf("加载配置: %w", err)
    }
    return data, nil
}

func main() {
    _, err := loadConfig()
    if err != nil {
        fmt.Println("错误信息:", err)
        // 加载配置: 读取文件 /etc/app/config.json: open /etc/app/config.json: no such file or directory

        // 可以通过 errors.Is 检查底层错误
        if errors.Is(err, os.ErrNotExist) {
            fmt.Println("根因:文件不存在")
        }
    }
}

4.2 %w 与 %v 的区别

package main

import (
    "errors"
    "fmt"
    "io"
)

func example_w() error {
    // %w 包装错误 -- 保留错误链,可以用 errors.Is/errors.As 查找
    return fmt.Errorf("操作失败: %w", io.EOF)
}

func example_v() error {
    // %v 仅格式化错误文本 -- 丢失错误链,无法用 errors.Is/errors.As 查找
    return fmt.Errorf("操作失败: %v", io.EOF)
}

func main() {
    err1 := example_w()
    fmt.Println("使用 %%w:", errors.Is(err1, io.EOF)) // true -- 错误链完整

    err2 := example_v()
    fmt.Println("使用 %%v:", errors.Is(err2, io.EOF)) // false -- 错误链断裂
}

4.3 多重包装(Go 1.20+)

Go 1.20 起,fmt.Errorf 支持多个 %w,可以同时包装多个错误:

package main

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

func complexOperation() error {
    err1 := os.ErrPermission
    err2 := io.ErrUnexpectedEOF

    // 同时包装两个错误
    return fmt.Errorf("操作失败: %w 且 %w", err1, err2)
}

func main() {
    err := complexOperation()
    fmt.Println(err) // 操作失败: permission denied 且 unexpected EOF

    // 两个底层错误都可以被 errors.Is 找到
    fmt.Println("是权限错误?", errors.Is(err, os.ErrPermission))   // true
    fmt.Println("是意外EOF?", errors.Is(err, io.ErrUnexpectedEOF)) // true
}

4.4 手动实现 Unwrap

package main

import (
    "errors"
    "fmt"
    "io"
)

// QueryError 包装底层错误的自定义类型
type QueryError struct {
    Query string
    Err   error
}

func (e *QueryError) Error() string {
    return fmt.Sprintf("查询 %q 失败: %v", e.Query, e.Err)
}

// Unwrap 让 errors.Is/errors.As 能沿链查找
func (e *QueryError) Unwrap() error {
    return e.Err
}

// JoinedError 包装多个底层错误(Go 1.20 风格)
type JoinedError struct {
    Message string
    Errs    []error
}

func (e *JoinedError) Error() string {
    return e.Message
}

// Unwrap 返回切片(Go 1.20+ 支持的多错误解包协议)
func (e *JoinedError) Unwrap() []error {
    return e.Errs
}

func main() {
    // 单错误链
    qErr := &QueryError{
        Query: "SELECT * FROM users",
        Err:   io.EOF,
    }
    fmt.Println(qErr)                        // 查询 "SELECT * FROM users" 失败: EOF
    fmt.Println(errors.Is(qErr, io.EOF))     // true

    // 多错误链
    jErr := &JoinedError{
        Message: "多个操作失败",
        Errs:    []error{io.EOF, io.ErrClosedPipe},
    }
    fmt.Println(errors.Is(jErr, io.EOF))           // true
    fmt.Println(errors.Is(jErr, io.ErrClosedPipe)) // true
}

5. errors.Is 和 errors.As

5.1 errors.Is -- 检查错误链中是否包含某个特定错误值

package main

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

var ErrDatabase = errors.New("数据库错误")

func queryDB() error {
    return fmt.Errorf("执行查询: %w", ErrDatabase)
}

func handleRequest() error {
    return fmt.Errorf("处理请求: %w", queryDB())
}

func main() {
    err := handleRequest()

    // errors.Is 会沿着错误链逐层查找
    // err -> "处理请求" -> "执行查询" -> ErrDatabase
    fmt.Println(errors.Is(err, ErrDatabase)) // true

    // 常见的标准库哨兵错误判断
    err2 := fmt.Errorf("读取结束: %w", io.EOF)
    fmt.Println(errors.Is(err2, io.EOF)) // true

    // 不在错误链中的错误
    fmt.Println(errors.Is(err, os.ErrNotExist)) // false

    // == 只能匹配同一个错误实例,不能沿链查找
    fmt.Println(err == ErrDatabase) // false(err 是包装后的,不等于原始值)
}

5.2 errors.As -- 从错误链中提取特定类型的错误

package main

import (
    "errors"
    "fmt"
    "net"
)

// APIError 自定义API错误
type APIError struct {
    StatusCode int
    Message    string
}

func (e *APIError) Error() string {
    return fmt.Sprintf("API错误 %d: %s", e.StatusCode, e.Message)
}

func callAPI() error {
    apiErr := &APIError{StatusCode: 403, Message: "禁止访问"}
    return fmt.Errorf("调用用户服务: %w", apiErr)
}

func main() {
    err := callAPI()

    // errors.As 从错误链中找到匹配类型的错误并赋值给 target
    var apiErr *APIError
    if errors.As(err, &apiErr) {
        fmt.Printf("状态码: %d, 消息: %s\n", apiErr.StatusCode, apiErr.Message)
        // 状态码: 403, 消息: 禁止访问
    }

    // 标准库中的实际应用:提取网络错误的详细信息
    _, netErr := net.Dial("tcp", "invalid-host:99999")
    if netErr != nil {
        var opErr *net.OpError
        if errors.As(netErr, &opErr) {
            fmt.Printf("网络操作: %s, 地址: %v\n", opErr.Op, opErr.Addr)
        }
    }
}

5.3 errors.Is 与 errors.As 的对比

package main

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

func main() {
    // 制造一个文件操作错误
    _, err := os.Open("/这个文件不存在")
    if err == nil {
        return
    }

    wrappedErr := fmt.Errorf("初始化失败: %w", err)

    // errors.Is: 用于匹配特定的错误【值】
    // 问题:这个错误是不是 os.ErrNotExist?
    if errors.Is(wrappedErr, os.ErrNotExist) {
        fmt.Println("Is: 文件不存在") // 匹配成功
    }

    // errors.As: 用于匹配特定的错误【类型】,并提取该类型的值
    // 问题:这个错误链中有没有 *os.PathError 类型的错误?
    var pathErr *os.PathError
    if errors.As(wrappedErr, &pathErr) {
        fmt.Printf("As: 路径=%s, 操作=%s\n", pathErr.Path, pathErr.Op)
        // As: 路径=/这个文件不存在, 操作=open
    }

    // 总结:
    // errors.Is(err, target)    -> 值比较,类似 err == target 但支持错误链
    // errors.As(err, &target)   -> 类型匹配,类似类型断言但支持错误链
}

5.4 自定义 Is 和 As 方法

package main

import (
    "errors"
    "fmt"
)

// ErrCode 基于错误码的错误类型
type ErrCode struct {
    Code    int
    Message string
}

func (e *ErrCode) Error() string {
    return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}

// Is 自定义比较逻辑:只要错误码相同就认为匹配
func (e *ErrCode) Is(target error) bool {
    t, ok := target.(*ErrCode)
    if !ok {
        return false
    }
    return e.Code == t.Code
}

func main() {
    err1 := &ErrCode{Code: 404, Message: "用户未找到"}
    err2 := &ErrCode{Code: 404, Message: "订单未找到"}
    err3 := &ErrCode{Code: 500, Message: "内部错误"}

    // 虽然消息不同,但错误码相同,Is 返回 true
    fmt.Println(errors.Is(err1, err2)) // true
    fmt.Println(errors.Is(err1, err3)) // false

    // 包装后仍然有效
    wrapped := fmt.Errorf("服务层: %w", err1)
    target := &ErrCode{Code: 404} // 只关心错误码
    fmt.Println(errors.Is(wrapped, target)) // true
}

6. 哨兵错误模式

6.1 什么是哨兵错误

哨兵错误(Sentinel Errors)是预定义的、包级别的错误变量,用作特定条件的标识符。调用者通过 errors.Is 来检查是否遇到了某个特定的已知错误。

package main

import (
    "errors"
    "fmt"
    "io"
    "bufio"
    "strings"
)

func main() {
    // 标准库中最常见的哨兵错误:io.EOF
    reader := strings.NewReader("Hello")
    scanner := bufio.NewScanner(reader)

    // io.EOF 表示数据读取完毕,不是真正的"错误"
    buf := make([]byte, 10)
    for {
        n, err := strings.NewReader("Hello").Read(buf)
        if errors.Is(err, io.EOF) {
            fmt.Printf("读取完毕,最后读取 %d 字节\n", n)
            break
        }
        if err != nil {
            fmt.Println("读取出错:", err)
            break
        }
        fmt.Printf("读取 %d 字节: %s\n", n, buf[:n])
    }

    _ = scanner
}

6.2 标准库中的哨兵错误示例

package main

import (
    "context"
    "database/sql"
    "errors"
    "fmt"
    "io"
    "os"
    "time"
)

func main() {
    // 1. io.EOF - 数据流结束
    fmt.Println("io.EOF:", io.EOF)

    // 2. io.ErrUnexpectedEOF - 读取到一半意外终止
    fmt.Println("io.ErrUnexpectedEOF:", io.ErrUnexpectedEOF)

    // 3. os.ErrNotExist - 文件不存在
    _, err := os.Stat("/不存在的文件")
    if errors.Is(err, os.ErrNotExist) {
        fmt.Println("文件不存在")
    }

    // 4. os.ErrPermission - 权限不足
    fmt.Println("os.ErrPermission:", os.ErrPermission)

    // 5. sql.ErrNoRows - 查询没有返回结果
    // 注意:这不是错误,只是表示没有匹配的行
    fmt.Println("sql.ErrNoRows:", sql.ErrNoRows)

    // 6. context.Canceled - 上下文被取消
    ctx, cancel := context.WithCancel(context.Background())
    cancel()
    fmt.Println("ctx.Err():", ctx.Err())
    fmt.Println("是 Canceled?", errors.Is(ctx.Err(), context.Canceled))

    // 7. context.DeadlineExceeded - 上下文超时
    ctx2, cancel2 := context.WithTimeout(context.Background(), 1*time.Nanosecond)
    defer cancel2()
    time.Sleep(1 * time.Millisecond)
    fmt.Println("是 DeadlineExceeded?", errors.Is(ctx2.Err(), context.DeadlineExceeded))
}

6.3 自定义哨兵错误

package main

import (
    "errors"
    "fmt"
)

// 包级别的哨兵错误,使用 errors.New 创建
var (
    ErrUserNotFound    = errors.New("用户未找到")
    ErrUserExists      = errors.New("用户已存在")
    ErrInvalidPassword = errors.New("密码无效")
    ErrAccountLocked   = errors.New("账户已锁定")
)

// UserStore 模拟用户存储
type UserStore struct {
    users map[string]string // username -> password
}

func NewUserStore() *UserStore {
    return &UserStore{
        users: map[string]string{
            "alice": "secret123",
        },
    }
}

func (s *UserStore) Register(username, password string) error {
    if _, exists := s.users[username]; exists {
        return fmt.Errorf("注册用户 %s: %w", username, ErrUserExists)
    }
    s.users[username] = password
    return nil
}

func (s *UserStore) Login(username, password string) error {
    stored, exists := s.users[username]
    if !exists {
        return fmt.Errorf("登录 %s: %w", username, ErrUserNotFound)
    }
    if stored != password {
        return fmt.Errorf("登录 %s: %w", username, ErrInvalidPassword)
    }
    return nil
}

func main() {
    store := NewUserStore()

    // 注册已存在的用户
    err := store.Register("alice", "newpass")
    if errors.Is(err, ErrUserExists) {
        fmt.Println("注册失败:用户已存在")
    }

    // 登录不存在的用户
    err = store.Login("bob", "pass")
    if errors.Is(err, ErrUserNotFound) {
        fmt.Println("登录失败:用户未找到")
    }

    // 密码错误
    err = store.Login("alice", "wrong")
    if errors.Is(err, ErrInvalidPassword) {
        fmt.Println("登录失败:密码错误")
    }

    // 使用 switch 分发错误处理
    err = store.Login("bob", "test")
    switch {
    case errors.Is(err, ErrUserNotFound):
        fmt.Println("-> 建议注册新账号")
    case errors.Is(err, ErrInvalidPassword):
        fmt.Println("-> 建议重置密码")
    case errors.Is(err, ErrAccountLocked):
        fmt.Println("-> 请联系管理员")
    case err != nil:
        fmt.Println("-> 未知错误:", err)
    default:
        fmt.Println("-> 登录成功")
    }
}

7. 错误处理最佳实践

7.1 何时使用 panic

panic 用于不可恢复的程序错误,而不是业务逻辑错误。

package main

import "fmt"

// 合理使用 panic 的场景

// 1. 程序初始化时的致命错误
func mustConnect(dsn string) {
    // 如果数据库连接失败,程序无法运行
    if dsn == "" {
        panic("数据库连接字符串不能为空")
    }
    // ... 连接数据库
}

// 2. Must 函数模式:将 error 转化为 panic
// 标准库中有 template.Must, regexp.MustCompile 等
func MustParseConfig(path string) map[string]string {
    config := map[string]string{"key": "value"} // 模拟解析
    if path == "" {
        panic("配置文件路径为空")
    }
    return config
}

// 3. 真正不可能发生的错误(程序逻辑bug)
func divide(a, b int) int {
    if b == 0 {
        // 如果调用者保证 b != 0,这里 panic 表示程序有 bug
        panic("除数为零:这是一个编程错误")
    }
    return a / b
}

func main() {
    // Must 模式通常在 init 或 main 的开头使用
    config := MustParseConfig("app.yaml")
    fmt.Println(config)
}

7.2 recover 捕获 panic

package main

import "fmt"

// safeExecute 安全执行一个函数,捕获可能的 panic
func safeExecute(fn func()) (err error) {
    defer func() {
        if r := recover(); r != nil {
            // 将 panic 转化为 error
            err = fmt.Errorf("捕获到 panic: %v", r)
        }
    }()

    fn()
    return nil
}

func riskyFunction() {
    panic("出大事了!")
}

func main() {
    // 场景:HTTP 中间件防止单个请求的 panic 导致服务崩溃
    err := safeExecute(riskyFunction)
    if err != nil {
        fmt.Println("安全捕获:", err)
        // 安全捕获: 捕获到 panic: 出大事了!
    }

    fmt.Println("程序继续运行...")
}

7.3 错误处理的黄金法则

package main

import (
    "fmt"
    "log"
)

// 规则1: 错误只应该被处理一次
// 要么处理它(记录日志、降级、返回默认值),要么向上传递
// 不要既记日志又返回错误

// 错误示范
func badExample() error {
    err := fmt.Errorf("something failed")
    if err != nil {
        log.Println("错误:", err) // 处理了一次(记日志)
        return err                 // 又返回了(让调用者再处理一次)
    }
    return nil
}

// 正确示范 A: 只向上传递(加上下文)
func goodExampleA() error {
    err := fmt.Errorf("something failed")
    if err != nil {
        return fmt.Errorf("执行操作X: %w", err) // 只传递,不记日志
    }
    return nil
}

// 正确示范 B: 只在这里处理
func goodExampleB() int {
    err := fmt.Errorf("something failed")
    if err != nil {
        log.Println("操作X失败,使用默认值:", err) // 处理:记日志+降级
        return 42                                    // 返回默认值
    }
    return 0
}

// 规则2: 总是为错误添加上下文
func fetchUserOrders(userID string) error {
    // 不好: return err
    // 好:   return fmt.Errorf("获取用户 %s 的订单: %w", userID, err)
    return fmt.Errorf("获取用户 %s 的订单: %w", userID,
        fmt.Errorf("数据库查询超时"))
}

// 规则3: 在顶层处理错误,底层只传递
func main() {
    // 这里是"顶层",负责最终处理
    if err := fetchUserOrders("u123"); err != nil {
        log.Println("请求处理失败:", err)
        // 可能还要返回 HTTP 500、发送告警等
    }

    _ = badExample()
    _ = goodExampleA()
    _ = goodExampleB()
}

7.4 不要忽略错误

package main

import (
    "fmt"
    "os"
)

func main() {
    // 糟糕:忽略了 Close 的错误(写入可能没有刷到磁盘)
    f, _ := os.Create("/tmp/test.txt")
    f.Write([]byte("data"))
    f.Close() // Close 可能返回错误!

    // 正确方式
    f2, err := os.Create("/tmp/test2.txt")
    if err != nil {
        fmt.Println("创建文件失败:", err)
        return
    }

    _, err = f2.Write([]byte("data"))
    if err != nil {
        f2.Close()
        fmt.Println("写入失败:", err)
        return
    }

    if err := f2.Close(); err != nil {
        fmt.Println("关闭文件失败(数据可能未完全写入):", err)
        return
    }

    // 如果确实要忽略某个错误,用 _ 显式忽略并加注释
    _ = os.Remove("/tmp/maybe-not-exists.txt") // 忽略: 文件可能不存在,不影响逻辑
}

8. 实际项目中的错误处理模式

8.1 分层错误设计

在一个分层架构中(Repository -> Service -> Handler),每层添加自己的上下文:

package main

import (
    "errors"
    "fmt"
)

// ========== 基础错误定义 ==========

var (
    ErrNotFound     = errors.New("资源未找到")
    ErrUnauthorized = errors.New("未授权")
    ErrInternal     = errors.New("内部错误")
)

// ========== Repository 层 ==========

type UserRepo struct{}

func (r *UserRepo) FindByID(id int) (string, error) {
    if id == 0 {
        return "", fmt.Errorf("UserRepo.FindByID(id=%d): %w", id, ErrNotFound)
    }
    if id < 0 {
        return "", fmt.Errorf("UserRepo.FindByID(id=%d): 数据库连接失败: %w", id, ErrInternal)
    }
    return "Alice", nil
}

// ========== Service 层 ==========

type UserService struct {
    repo *UserRepo
}

func (s *UserService) GetUserProfile(id int) (string, error) {
    name, err := s.repo.FindByID(id)
    if err != nil {
        return "", fmt.Errorf("UserService.GetUserProfile: %w", err)
    }
    return fmt.Sprintf("用户资料: %s", name), nil
}

// ========== Handler 层 ==========

type UserHandler struct {
    service *UserService
}

func (h *UserHandler) HandleGetUser(id int) {
    profile, err := h.service.GetUserProfile(id)
    if err != nil {
        // 在最顶层根据错误类型决定如何响应
        switch {
        case errors.Is(err, ErrNotFound):
            fmt.Printf("HTTP 404: %v\n", err)
        case errors.Is(err, ErrUnauthorized):
            fmt.Printf("HTTP 401: %v\n", err)
        case errors.Is(err, ErrInternal):
            fmt.Printf("HTTP 500: %v\n", err)
        default:
            fmt.Printf("HTTP 500: 未知错误: %v\n", err)
        }
        return
    }
    fmt.Printf("HTTP 200: %s\n", profile)
}

func main() {
    handler := &UserHandler{
        service: &UserService{
            repo: &UserRepo{},
        },
    }

    handler.HandleGetUser(1)  // HTTP 200: 用户资料: Alice
    handler.HandleGetUser(0)  // HTTP 404: UserService.GetUserProfile: UserRepo.FindByID(id=0): 资源未找到
    handler.HandleGetUser(-1) // HTTP 500: UserService.GetUserProfile: UserRepo.FindByID(id=-1): 数据库连接失败: 内部错误
}

8.2 错误码设计模式

package main

import (
    "errors"
    "fmt"
)

// AppError 统一的应用错误类型
type AppError struct {
    Code    string // 业务错误码,如 "USER_001"
    Message string // 面向用户的消息
    Err     error  // 底层错误(可选)
}

func (e *AppError) Error() string {
    if e.Err != nil {
        return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Err)
    }
    return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}

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

// Is 允许通过错误码进行匹配
func (e *AppError) Is(target error) bool {
    t, ok := target.(*AppError)
    if !ok {
        return false
    }
    return e.Code == t.Code
}

// HTTPStatus 根据错误码前缀返回 HTTP 状态码
func (e *AppError) HTTPStatus() int {
    switch e.Code[:3] {
    case "VAL": // 验证错误
        return 400
    case "AUT": // 认证错误
        return 401
    case "FBD": // 权限错误
        return 403
    case "NTF": // 未找到
        return 404
    default:
        return 500
    }
}

// 预定义的错误模板
var (
    ErrCodeValidation  = &AppError{Code: "VAL_001"}
    ErrCodeNotFound    = &AppError{Code: "NTF_001"}
    ErrCodeAuth        = &AppError{Code: "AUT_001"}
    ErrCodeInternal    = &AppError{Code: "INT_001"}
)

// NewNotFoundError 创建一个未找到错误
func NewNotFoundError(resource string, id interface{}) *AppError {
    return &AppError{
        Code:    "NTF_001",
        Message: fmt.Sprintf("%s (ID=%v) 不存在", resource, id),
    }
}

// NewValidationError 创建一个验证错误
func NewValidationError(field, reason string) *AppError {
    return &AppError{
        Code:    "VAL_001",
        Message: fmt.Sprintf("字段 %s 验证失败: %s", field, reason),
    }
}

// NewInternalError 创建一个内部错误(包装底层错误)
func NewInternalError(msg string, cause error) *AppError {
    return &AppError{
        Code:    "INT_001",
        Message: msg,
        Err:     cause,
    }
}

func processOrder(userID, productID int) error {
    if userID <= 0 {
        return NewValidationError("userID", "必须大于0")
    }
    if productID == 99 {
        return NewNotFoundError("商品", productID)
    }
    if productID < 0 {
        return NewInternalError("查询商品失败",
            fmt.Errorf("数据库超时"))
    }
    return nil
}

func handleRequest(userID, productID int) {
    err := processOrder(userID, productID)
    if err == nil {
        fmt.Println("200 OK: 订单创建成功")
        return
    }

    // 从错误中提取 AppError
    var appErr *AppError
    if errors.As(err, &appErr) {
        fmt.Printf("HTTP %d | 错误码: %s | %s\n",
            appErr.HTTPStatus(), appErr.Code, appErr.Message)
    } else {
        fmt.Println("HTTP 500 | 未知错误:", err)
    }
}

func main() {
    handleRequest(1, 1)    // 200 OK: 订单创建成功
    handleRequest(0, 1)    // HTTP 400 | 错误码: VAL_001 | 字段 userID 验证失败: 必须大于0
    handleRequest(1, 99)   // HTTP 404 | 错误码: NTF_001 | 商品 (ID=99) 不存在
    handleRequest(1, -1)   // HTTP 500 | 错误码: INT_001 | 查询商品失败

    // 使用 errors.Is 通过错误码模板匹配
    err := processOrder(0, 1)
    fmt.Println("\n是验证错误?", errors.Is(err, ErrCodeValidation)) // true
    fmt.Println("是未找到?", errors.Is(err, ErrCodeNotFound))       // false
}

8.3 defer 与错误处理的结合

package main

import (
    "fmt"
    "os"
)

// writeToFile 演示使用 defer 和命名返回值处理关闭错误
func writeToFile(path, content string) (err error) {
    f, err := os.Create(path)
    if err != nil {
        return fmt.Errorf("创建文件: %w", err)
    }

    // 使用 defer + 命名返回值来处理 Close 错误
    defer func() {
        closeErr := f.Close()
        if err == nil {
            // 如果写入没有出错,但 Close 出错了,返回 Close 的错误
            err = closeErr
        }
        // 如果写入已经出错了,Close 的错误就忽略(保留原始错误)
    }()

    _, err = f.WriteString(content)
    if err != nil {
        return fmt.Errorf("写入内容: %w", err)
    }

    return nil
}

// transaction 模拟数据库事务的错误处理模式
func transaction(shouldFail bool) (err error) {
    fmt.Println("开始事务")

    defer func() {
        if err != nil {
            fmt.Println("回滚事务")
            // rollback()
        } else {
            fmt.Println("提交事务")
            // commit()
        }
    }()

    fmt.Println("执行操作1")
    if shouldFail {
        return fmt.Errorf("操作1失败")
    }
    fmt.Println("执行操作2")

    return nil
}

func main() {
    // 写文件
    if err := writeToFile("/tmp/test_defer.txt", "Hello, Go!"); err != nil {
        fmt.Println("写文件失败:", err)
    } else {
        fmt.Println("写文件成功")
    }

    // 事务成功
    fmt.Println("\n--- 事务成功 ---")
    transaction(false)

    // 事务失败
    fmt.Println("\n--- 事务失败 ---")
    transaction(true)
}

8.4 使用 errors.Join(Go 1.20+)

package main

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

// cleanup 清理多个资源,收集所有错误
func cleanup(files []*os.File) error {
    var errs []error
    for _, f := range files {
        if err := f.Close(); err != nil {
            errs = append(errs, fmt.Errorf("关闭 %s: %w", f.Name(), err))
        }
    }
    // errors.Join 将多个错误合并为一个
    // 如果 errs 为空,返回 nil
    return errors.Join(errs...)
}

func main() {
    // errors.Join 基本用法
    err := errors.Join(
        fmt.Errorf("错误1: 连接超时"),
        fmt.Errorf("错误2: 写入失败"),
        nil, // nil 会被自动忽略
        fmt.Errorf("错误3: 缓存未命中"),
    )

    fmt.Println("合并后的错误:")
    fmt.Println(err)
    // 错误1: 连接超时
    // 错误2: 写入失败
    // 错误3: 缓存未命中

    // errors.Join 返回的错误也支持 errors.Is
    sentinel := errors.New("特殊错误")
    joined := errors.Join(
        fmt.Errorf("包装: %w", sentinel),
        fmt.Errorf("另一个错误"),
    )
    fmt.Println("\n包含特殊错误?", errors.Is(joined, sentinel)) // true
}

总结

场景 推荐方式
函数可能失败 返回 (result, error)
添加上下文 fmt.Errorf("上下文: %w", err)
检查特定错误值 errors.Is(err, target)
提取特定错误类型 errors.As(err, &target)
定义已知错误条件 包级别哨兵变量 var ErrXxx = errors.New(...)
定义携带数据的错误 自定义结构体实现 error 接口
不可恢复的错误 panic(仅限程序 bug 或初始化失败)
合并多个错误 errors.Join(Go 1.20+)
一次包装多个错误 fmt.Errorf("... %w ... %w", err1, err2)(Go 1.20+)

主题测试文章,只做测试使用。发布者:Walker,转转请注明出处:https://www.walker-learn.xyz/archives/6719

(0)
Walker的头像Walker
上一篇 2026年3月6日 22:00
下一篇 2026年3月6日 21:00

相关推荐

  • Go工程师体系课 004

    需求分析 后台管理系统 商品管理 商品列表 商品分类 品牌管理 品牌分类 订单管理 订单列表 用户信息管理 用户列表 用户地址 用户留言 轮播图管理 电商系统 登录页面 首页 商品搜索 商品分类导航 轮播图展示 推荐商品展示 商品详情页 商品图片展示 商品描述 商品规格选择 加入购物车 购物车 商品列表 数量调整 删除商品 结算功能 用户中心 订单中心 我的…

    2026年3月7日
    7300
  • 编程基础 0006_并发进阶_sync包与Context

    并发进阶:sync 包与 Context 一、sync 包详解 1. sync.Mutex 与 sync.RWMutex // Mutex: 互斥锁,同一时间只有一个 goroutine 能持有 var mu sync.Mutex var count int func increment() { mu.Lock() defer mu.Unlock() cou…

    后端开发 2026年3月6日
    7100
  • Go资深工程师讲解(慕课) 007_godoc与代码生成

    Go 文档生成与示例代码 对应视频 8-6 生成文档和示例代码 1. godoc 文档生成 Go 的文档直接从源码注释中提取,不需要特殊标记语法。 1.1 注释规范 // Package queue 实现了一个简单的 FIFO 队列。 // // 该队列基于切片实现,支持 Push、Pop 和 IsEmpty 操作。 package queue // Que…

    后端开发 2026年3月6日
    7800
  • Go工程师体系课 001

    转型 想在短时间系统转到Go工程理由 提高CRUD,无自研框架经验 拔高技术深度,做专、做精需求的同学 进阶工程化,拥有良好开发规范和管理能力的 工程化的重要性 高级开的期望 良好的代码规范 深入底层原理 熟悉架构 熟悉k8s的基础架构 扩展知识广度,知识的深度,规范的开发体系 四个大的阶段 go语言基础 微服务开发的(电商项目实战) 自研微服务 自研然后重…

    后端开发 2026年3月6日
    7000
  • Go资深工程师讲解(慕课) 000_课程目录索引

    Google资深工程师深度讲解Go语言 - 课程目录索引 课程来源:慕课网(百度网盘备份)讲师风格:从 Google 工程实践出发,注重底层原理和工程规范 完整视频章节与笔记对照表 章节 视频文件 笔记位置 状态 Ch1 课程介绍 1-1 课程导读 — 跳过 1-2 安装与环境 001.md > GOPATH、环境变量 已覆盖 Ch2 基础语法 2-1…

    后端开发 2026年3月6日
    7100
简体中文 繁体中文 English