编程基础 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://walker-learn.xyz/archives/6719

(0)
Walker的头像Walker
上一篇 14小时前
下一篇 13小时前

相关推荐

  • Go工程师体系课 005

    微服务开发 创建一个微服务项目,所有的项目微服务都在这个项目中进行,创建joyshop_srv,我们无创建用户登录注册服务,所以我们在项目目录下再创建一个目录user_srv 及user_srv/global(全局的对象新建和初始化)user_srv/handler(业务逻辑代码)user_srv/model(用户相关的 model)user_srv/pro…

  • Go工程师体系课 protoc-gen-validate

    protoc-gen-validate 简介与使用指南 ✅ 什么是 protoc-gen-validate protoc-gen-validate(简称 PGV)是一个 Protocol Buffers 插件,用于在生成的 Go 代码中添加结构体字段的验证逻辑。 它通过在 .proto 文件中添加 validate 规则,自动为每个字段生成验证代码,避免你手…

  • Go资深工程师讲解(慕课) 000_课程目录索引

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

    后端开发 23小时前
    400
  • Go工程师体系课 007

    商品微服务 实体结构说明 本模块包含以下核心实体: 商品(Goods) 商品分类(Category) 品牌(Brands) 轮播图(Banner) 品牌分类(GoodsCategoryBrand) 1. 商品(Goods) 描述平台中实际展示和销售的商品信息。 字段说明 字段名 类型 说明 name String 商品名称,必填 brand Pointer …

  • Go工程师体系课 010

    es 安装 elasticsearch(理解为库) kibana(理解为连接工具)es 和 kibana(5601) 的版本要保持一致 MySQL 对照学习 Elasticsearch(ES) 术语对照 MySQL Elasticsearch database index(索引) table type(7.x 起固定为 _doc,8.x 彻底移除多 type…

    后端开发 7小时前
    400
简体中文 繁体中文 English