Golang错误机制
侧边栏壁纸
  • 累计撰写 11 篇文章
  • 累计收到 1 条评论

Golang错误机制

tqtqtq
2026-02-27 / 0 评论 / 2 阅读 / 正在检测是否收录...

Golang错误机制

Go的错误处理哲学

Go的设计者认为:错误是常态,应该显式处理

// Go的方式:错误是返回值的一部分,你必须处理
result, err := 做某事()
if err != nil {
    // 出错了!必须在这里处理
    return
}
// 没出错,继续使用result

核心原则不隐藏错误,强制开发者面对问题

在 Go 语言中,错误(Error)就像普通的数据一样被服务员平平静静地“递”回来。Go 鼓励程序员在每一步都主动检查:“刚才的任务成功了吗?”

error接口——错误的本质

在Go中,error就是一个接口

// error接口的定义(内置在Go中)
type error interface {
    Error() string  // 只要实现了Error()方法,就是error
}

任何类型,只要有一个 Error() string 方法,就可以当作错误使用。

// 定义一个包含状态码的专属错误
type MyVIPError struct {
    Code    int    // 错误代码,比如 404
    Message string // 错误信息
}

// 只要它有 "Error()" ,Go 就认它是一个错误
func (e *MyVIPError) Error() string {
    // 把代码和信息拼起来
    return fmt.Sprintf("错误码: %d, 原因: %s", e.Code, e.Message)
}

创建最基本的错误

package main

import (
    "errors"  // 标准库提供的错误工具包
    "fmt"
)

func main() {
    // 方式1:使用errors.New创建简单错误
    err1 := errors.New(" Something went wrong")
    fmt.Println(err1)  // 输出: Something went wrong
    
    // 方式2:使用fmt.Errorf创建格式化错误
    name := "Alice"
    age := -5
    err2 := fmt.Errorf("invalid age: %d for user %s", age, name)
    fmt.Println(err2)  // 输出: invalid age: -5 for user Alice
}
  • errors.New = 写一张简单的便签
  • fmt.Errorf = 写一张可以填入信息的模板便签

函数如何返回错误

多返回值

Go函数可以一次返回多个值,这是错误处理的基础:

package main

import (
    "errors"
    "fmt"
)

// 函数返回两个值:(结果, 错误)
// 如果出错,结果通常为零值,错误不为nil
// 如果成功,错误为nil
func divide(a, b float64) (float64, error) {
    if b == 0 {
        // 除数为0,返回错误
        return 0, errors.New("cannot divide by zero")
    }
    // 正常情况,错误返回nil
    return a / b, nil
}

func main() {
    // 调用函数,接收两个返回值
    result, err := divide(10, 2)
    if err != nil {
        fmt.Println("计算失败:", err)
        return
    }
    fmt.Printf("10 / 2 = %.2f\n", result)  // 输出: 10 / 2 = 5.00
    
    // 测试错误情况
    result2, err2 := divide(10, 0)
    if err2 != nil {
        fmt.Println("计算失败:", err2)  // 输出: 计算失败: cannot divide by zero
        return
    }
    fmt.Printf("10 / 0 = %.2f\n", result2)
}

错误处理的标准模式

// 标准5步曲
result, err := 某个函数()
if err != nil {
    // 1. 记录日志
    // 2. 清理资源
    // 3. 返回错误(或降级处理)
    return err
}
// 4. 使用结果
使用(result)
// 5. 继续后续操作

关键约定

  • 错误是最后一个返回值
  • 错误变量名永远叫err(约定俗成)
  • 成功时 err == nil,失败时 err != nil

错误的判断与分类

判断错误是否发生

if err != nil {
    // 有错误!
}

判断具体是什么错误

方式一:错误值比较(Sentinel Errors)

预定义一些"标准错误",像常量一样使用:

package main

import (
    "errors"
    "fmt"
)

// 在包级别定义标准错误(哨兵错误)
// 习惯用 ErrXXX 命名
var (
    ErrNotFound       = errors.New("resource not found")
    ErrInvalidInput   = errors.New("invalid input")
    ErrPermissionDenied = errors.New("permission denied")
    ErrTimeout        = errors.New("operation timeout")
)

// 模拟数据库查询
func findUser(id int) (string, error) {
    if id < 1 {
        return "", ErrInvalidInput  // 返回特定错误
    }
    if id == 999 {
        return "", ErrNotFound     // 返回特定错误
    }
    return "User" + fmt.Sprint(id), nil
}

func main() {
    // 测试不同错误
    testCases := []int{-5, 999, 42}
    
    for _, id := range testCases {
        user, err := findUser(id)
        
        if err != nil {
            // 判断具体是哪种错误
            if err == ErrInvalidInput {
                fmt.Printf("ID=%d: 输入无效,请检查参数\n", id)
            } else if err == ErrNotFound {
                fmt.Printf("ID=%d: 用户不存在,显示404页面\n", id)
            } else {
                fmt.Printf("ID=%d: 未知错误: %v\n", id, err)
            }
            continue
        }
        
        fmt.Printf("ID=%d: 找到用户 %s\n", id, user)
    }
}

输出

ID=-5: 输入无效,请检查参数
ID=999: 用户不存在,显示404页面
ID=42: 找到用户 User42

方式二:errors.Is(Go 1.13+ 推荐)

问题:错误可能被包装,直接比较会失败。

package main

import (
    "errors"
    "fmt"
)

var ErrNotFound = errors.New("not found")

func findInDB() error {
    return ErrNotFound
}

func findInCache() error {
    return fmt.Errorf("cache miss: %w", ErrNotFound)  // 包装错误
}

func main() {
    err := findInCache()
    
    // 直接比较:失败!因为err是包装后的
    fmt.Println("直接比较:", err == ErrNotFound)  // false
    
    // 使用errors.Is:成功!会自动"拆包装"
    fmt.Println("errors.Is:", errors.Is(err, ErrNotFound))  // true
}

errors.Is的作用:像剥洋葱一样,一层层拆开包装,直到找到目标错误。

方式三:errors.As(获取错误的具体类型)

在 Go 1.13 版本之前,如果遇到错误,我们通常只是简单地返回它。但有时候,为了增加更多的上下文信息(比如是在哪个函数、哪个步骤出错的),我们会把原始错误“包装”起来。

以前,直接用类型断言 err.(*PathError) 只能检查最外层。如果错误被套在了里面,断言就会失败。 errors.As 就是为了解决这个问题而诞生的。

errors.As 是一个专门用来“拆套娃并提取内容”的工具。

它的工作原理是:

  1. 剥开外衣:它会自动一层一层地剥开(Unwrap)错误套娃。
  2. 寻找目标:在每一层检查,看当前的错误是不是你想要的那个“特定类型”。
  3. 赋值提取:如果找到了,它就把那个错误赋值给你准备好的变量,并返回 true。如果剥到最里面都没找到,就返回 false

当需要调用错误类型的方法访问其字段时:

package main

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

func main() {
    // 模拟一个网络错误
    err := &net.OpError{
        Op:   "dial",
        Net:  "tcp",
        Err:  errors.New("connection refused"),
    }
    
    // 我们想判断这是否是网络错误,并获取详细信息
    
    // 方式1:类型断言(旧方式,不推荐)
    if netErr, ok := err.(*net.OpError); ok {
        fmt.Printf("网络操作: %s, 网络类型: %s\n", netErr.Op, netErr.Net)
    }
    
    // 方式2:errors.As(推荐,支持错误链)
    var netErr *net.OpError
    // 参数1: 被包装的总错误 (套娃)
    // 参数2: 目标变量的指针 (必须传指针的地址 &)
    if errors.As(err, &netErr) {
        fmt.Printf("操作: %s, 类型: %s\n", netErr.Op, netErr.Net)
    }
}

初学者在使用 errors.As 时,最容易在第二个参数上犯错。请记住这个口诀:“一定要传目标变量的指针(地址)”

  • 为什么? 因为 errors.As 是一个函数,如果你只传变量的值进去,它是无法修改你外面的变量的(Go 是值传递)。只有把变量的“内存地址”交出去(用 &),errors.As 才能顺着地址把找到的错误塞进你的变量里。

错误的创建与包装

创建错误

package main

import (
    "errors"
    "fmt"
)

func main() {
    // 方法1:简单错误
    err1 := errors.New("something went wrong")
    
    // 方法2:格式化错误
    err2 := fmt.Errorf("user %s not found", "Alice")
    
    // 方法3:包装错误(保留原始错误)
    original := errors.New("connection refused")
    wrapped := fmt.Errorf("database connection failed: %w", original)
    
    fmt.Println(err1)
    fmt.Println(err2)
    fmt.Println(wrapped)
    // 输出: database connection failed: connection refused
}

错误包装的艺术

为什么要包装?

  • 添加上下文(在哪里出错的)
  • 保留原始错误(用于后续判断)
  • 构建错误链
package main

import (
    "errors"
    "fmt"
)

var ErrNotFound = errors.New("record not found")

// 底层函数
func queryDB(id string) error {
    return fmt.Errorf("database query failed for id=%s: %w", id, ErrNotFound)
}

// 中层函数
func getUser(id string) error {
    if err := queryDB(id); err != nil {
        return fmt.Errorf("getUser failed: %w", err)
    }
    return nil
}

// 顶层函数
func handleRequest(id string) error {
    if err := getUser(id); err != nil {
        return fmt.Errorf("request processing failed: %w", err)
    }
    return nil
}

func main() {
    err := handleRequest("123")
    
    fmt.Println("=== 完整错误链 ===")
    fmt.Println(err)
    // 输出: request processing failed: getUser failed: database query failed for id=123: record not found
    
    fmt.Println("\n=== 判断根因 ===")
    if errors.Is(err, ErrNotFound) {
        fmt.Println("根本原因是:记录不存在")
    }
    
    fmt.Println("\n=== 逐层展开 ===")
    // 手动展开(实际很少需要)
    current := err
    for current != nil {
        fmt.Printf("→ %v\n", current)
        current = errors.Unwrap(current)
    }
}

输出

=== 完整错误链 ===
request processing failed: getUser failed: database query failed for id=123: record not found

=== 判断根因 ===
根本原因是:记录不存在

=== 逐层展开 ===
→ request processing failed: getUser failed: database query failed for id=123: record not found
→ getUser failed: database query failed for id=123: record not found
→ database query failed for id=123: record not found
→ record not found

关键:使用 %w 动词包装错误,才能用 errors.Iserrors.As 追溯。

自定义错误类型

为什么需要自定义错误?

标准错误只是一段文字,但有时我们需要:

  • 错误码(给程序判断)
  • HTTP状态码(给API返回)
  • 详细信息(给日志记录)
  • 堆栈信息(给调试)

创建自定义错误类型

package main

import (
    "fmt"
    "time"
)

// 定义一个结构体作为错误类型
type AppError struct {
    Code      int       // 错误码
    Message   string    // 给用户看的消息
    Detail    string    // 给开发者看的详情
    Timestamp time.Time // 发生时间
    Path      string    // 发生位置
}

// 实现error接口(这是关键!)
func (e *AppError) Error() string {
    return fmt.Sprintf("[%d] %s: %s (at %s)", 
        e.Code, e.Message, e.Detail, e.Path)
}

// 构造函数
func NewAppError(code int, message, detail, path string) *AppError {
    return &AppError{
        Code:      code,
        Message:   message,
        Detail:    detail,
        Timestamp: time.Now(),
        Path:      path,
    }
}

// 使用示例
func doSomething() error {
    return NewAppError(
        1001,                           // 错误码
        "操作失败",                      // 用户消息
        "数据库连接超时超过30秒",          // 详细原因
        "/api/v1/users",                // 发生位置
    )
}

func main() {
    err := doSomething()
    
    // 作为普通错误打印
    fmt.Println(err)
    // 输出: [1001] 操作失败: 数据库连接超时超过30秒 (at /api/v1/users)
    
    // 类型断言获取详细信息
    if appErr, ok := err.(*AppError); ok {
        fmt.Printf("\n错误码: %d\n", appErr.Code)
        fmt.Printf("用户提示: %s\n", appErr.Message)
        fmt.Printf("发生时间: %v\n", appErr.Timestamp)
    }
}
0

评论 (0)

取消