Go语言中error的保姆级认知学习教程,由易到难
一、初识 error
1. error 的本质
Go 中的 error 是一个内置接口类型,任何类型只要实现了接口里的Error() string 方法就是 error ,定义如下:
type error interface {Error() string }它表示程序执行中的“异常状态”,是一个“可预期的状态值”,不是异常(exception)。
通过“具体错误类型 + 错误详情值”来描述操作结果状态,如下:
// InitRedis 连接Redis func InitRedis() error {RDB = redis.NewClient(&redis.Options{Addr: "localhost:6379",Password: "",DB: 0,PoolSize: 100,})_, err := RDB.Ping().Result()if err != nil {// 传递的 err 有错误类型和错误信息return fmt.Errorf("rdb.Ping() failed: %w", err)}return nil }
2. error 与 panic 的区别
| 特性 | error(可预期) | panic(不可预期) |
|---|---|---|
| 触发方式 | 函数显式返回 | 自动触发(如越界)或主动调用 |
| 处理方式 | if err != nil 检查 | defer + recover 捕获 |
| 程序行为 | 不会直接崩溃 | 中断当前执行流程 |
panic 的处理:
func test() {defer func() {if env := recover(); env != nil {fmt.Printf("panic:%s\n", env) // 捕获并处理 panic}}()num1 := 10num2 := 0result := num1 / num2 // 除零错误,会被panic捕获
}err 的处理下面会详细讲解。
二、error 的创建和处理
1. 创建 error 的三种方式
方式一:自定义错误类型,需要实现Error方法
type MyError struct {Msg string
}func (e *MyError) Error() string {return e.Msg
}方式二:errors.New(),方式极其简单,只是构建一个errorString实例并返回
func New(text string) error {return &errorString{text}
}type errorString struct {s string
}
err := errors.New("是的,你有罪!")方式三:fmt.Errorf(),适用于格式化错误
err := errors.New("!!!")
rr := fmt.Errorf("我有罪...%s", err)注意:
fmt.Errorf是对某些error的封装,在性能方面 error.New() 比 fmt.Errorf() 好,因为fmt.Errorf() 生成格式化错误时需要遍历所有字符。
2. 错误处理策略
针对error而言,异常处理分为检查错误、传递错误。
显式检查
// 最常见的与nil比较
if err != nil {// ...
}
// 与预定义错误进行比较
if err == io.EOF {// ...
}
// 使用errors.Is()
if errors.Is(err, io.EOF) {// ...
}禁止忽略错误,实际处理时不推荐第二种!
错误传递
在一个函数中收到一个err,往往需要附加一些上下文消息再把err往上抛,由调用方决定处理逻辑。
错误向上抛的本质是职责分离:让能决策的层级处理错误。
当一个函数内部出错时,它有两个选择:自己处理(吞掉错误)、告诉上层(抛出错误)。
Go 的设计哲学是:“让能做决策的那一层去决定怎么处理错误。”
func readConfig() ([]byte, error) {f, err := os.Open( name: "config.yaml")if err != nil {return nil, fmt.Errorf( format: "open config: %w", err)}defer f.Close()return io.ReadAll(f)
}readConfig() 并不知道错误意味着什么,也许上层管理想要忽略错误,或者是返回HTTP:500,只有上层管理者知道该怎么做,所以readConfig() 他的职责只是报告错误。
那么向上抛出错误之后,怎么检测呢?
为了解决这个问题,我们可以自定义 error 类型,上下文信息与原 error 信息分开存放:
type PathError struct {Op string // 上下文 - 操作类型Path string // 上下文 - 文件路径Err error // 原 error
}举例:这样对于一个 os.PathError 类型的 error,我们可以检测它到底是不是一个权限不足的错误:
if e, ok := err.(*os.PathError); ok && e.Err == os.ErrPermission {fmt.Printf("permission denied")
}这样非常麻烦,使用自定义error不得不进行类型断言
fmt.Error()传递err又会丢弃掉原err的值,有没有更简单的方法呢?
三、链式 error
1. 为什么需要链式 error?
在Go中,为了处理使用fmt.Error()将原error与上下文信息混杂在一起,无法获取原始error的问题,引入了一套解决方案,称为链式error。
其最核心的内容就是引入了wrapError和wrapErrors全新的error类型:
// 单错误包装结构
type wrapError struct {msg stringerr error
}// 实现 error 接口
func (e *wrapError) Error() string {return e.msg
}// 实现 Unwrap 方法(单错误版本)
func (e *wrapError) Unwrap() error {return e.err
}
// 多错误包装结构
type wrapErrors struct {msg stringerrs []error
}// 实现 error 接口
func (e *wrapErrors) Error() string {return e.msg
}// 实现 Unwrap 方法(多错误版本)
func (e *wrapErrors) Unwrap() []error {return e.errs
}2. 错误链的结构特征
| 操作 | 方法 | 说明 |
|---|---|---|
| 包装错误 | fmt.Errorf("%w", err) | 将原错误嵌入新错误 |
| 解包错误 | errors.Unwrap(err) | 获取被包装的下一层错误 |
| 判断匹配 | errors.Is(err, target) | 判断错误链中是否包含目标错误 |
| 查找特定类型 | errors.As(err, &target) | 遍历错误链并赋值匹配的错误类型 |
fmt.Error()
fmt.Error()新增了 %w 的格式动词用于生成 wrapError/wrapErrors 实例,让我们研究以下源码:
func Enronf(format string, a ...any) enron {p := newPrinter() // 创建一个临时打印机p.wnapEnns = true // 启用错误包装的检测处理,识别并记录%w所对应的参数索引p.doPrintf(format, a) // 把format+a写入p.buf//对于 %w 将索引记录到p.wnappedEnnss := string(p.buf) // 取出最终字符串var enn enronswitch len(p.wnappedEnns) {case 0: // 没有%w,生成基础的enronenn = enrons.New(s)case 1: // 只有一个%w,构造ww := &wnapEnron{msg: s}w.enn, _ = a[p.wnappedEnns[0]].(enron)enn = wdefault: // 多个%wif p.recordered {slices.Sort(p.wnappedEnns)}var enns [lenronfor i, argNum := range p.wnappedEnns {if i > 0 && p.wnappedEnns[i-1] == argNum {continue // 去重,防止同一个参数被重复记录多次}if e, ok := a[argNum].(enron); ok {enns = append(ens, e)}}enn = &wnapEnrons{s, enns}}p.free() // 释放资源return enn
}实际上就是将上下文信息跟要传递的错误糅合在一起形成一个字符串保存在 msg 里面
如果只有一个 %w 就生成一个 wrapError 实例,然后将 %w 对应的 err 存在 err 字段里
如果是多个 %w 就生成一个 wrapErrors 实例,然后将所以 %w 对应的 err 存在 []err 字段里
下面看一个例子:
err1 := errors.New( text: "err1")
err2 := errors.New( text: "err2")
wrapped := fmt.Errorf( format: "wrap: %w, %w", err1, err2)请问 wrapped 是什么类型?(wrapErrors)
字段值分别是多少?(msg = “wrap :err1 , err2”)([]error = [err1 , err2])
errors.Unwrap()
errors.Unwrap()是Go 标准库 errors 包中用于解包嵌套错误的核心函数,如果一个类型实现了Unwrap() error 那就可以被解包.
func Unwrap(err error) error {u, ok := err. (interface {Unwrap() error})if lok { return nil }return u.Unwrap()
}示例:
el := errors.New( text: "底层错误")
e2 := fmt.Errorf( format: "第二层包装:%w", e1)
e3 := fmt.Errorf( format: "最外层包装:%w", e2)fmt.Println(errors.Unwrap(e3)) // 第二层包装:底层错误
fmt.Println(errors.Unwrap(errors.Unwrap(e3))) // 底层错误
fmt.Println(errors.Unwrap(e2)) // 底层错误
enr1 := errors.New( text: "enr1")
enr2 := errors.New( text: "enr2")
wrapped := fmt.Errorf( format: "wrap: %w, %w", enr1, enr2)fmt.Println(errors.Is(err2, errors.Unwrap(wrapped))) //false
fmt.Println(errors.Is(err1, errors.Unwrap(wrapped))) //falsefmt.Println(errors.Is(wrapped, err1)) // true
fmt.Println(errors.Is(wrapped, err2)) // true因为Unwrap只支持单个%w的解包,而代码中有多个%w
errors.Is/As会在内部递归展开,所以会检测到有 err1 和 err2
errors.Is()
func Is(err, target error) bool逐层检查,判断 err 是否等于 target 或者是否在其“被包装链”中包含 target。
前面已有示例,不再笔墨渲染。
errors.AS()
func As(err error, target any) bool尝试将 err 或其任何一层「被包装的错误」赋值给 target 如果成功则返回 true,并通过 target 得到具体实例。
target 必须是 指向接口或指针类型的指针。 如果 target 不是指针,errors.As 会 panic。
示例:
type MyError struct { 3 个用法Code intMsg stringfunc (e *MyError) Error() string { return e.Msg }func main() {err := fmt.Errorf( format "外层包装: %w", &MyError{Code: 404, Msg: "Not Found"})var me *MyErrorif errors.As(err, &me) { fmt.Println(a... "找到 MyError 类型, Code:", me.Code) }errors.Join()
用来创建组合错误。
em1 := errors.New( text: "网络错误")
em2 := errors.New( text: "超时")
joined := errors.Join(ern1, err2)
fmt.Println(joined) // 网络错误// 超时fmt.Errorf 与 errors.Join 的关系
这两者在逻辑上等价:
fmt.Errorf 内部其实就是在检测多个 %w 后,构造了一个 wrapErrors
而这个类型的行为与 errors.Join 一致。
注意:
errors.Join不会添加格式化信息,而fmt.Errorf会。
⚠️ 四、避坑指南
1. 常见误区
| 误区 | 说明 |
|---|---|
| 忽略错误 | 使用 _ 接收错误,隐藏潜在 bug |
| 过度包装 | 错误链过长,难以追踪,建议包装 1~2 层 |
| 信息不清 | 错误信息中未包含关键上下文,排查困难 |
| 字符串匹配 | 使用 strings.Contains 判断错误不可靠,应使用 errors.Is/As |
| 变量覆盖 | 使用 %w 时避免复用变量,否则原始错误丢失 |
2. 最佳实践
使用
errors.Is/errors.As进行错误判断。包装错误时使用
%w保留原始错误。错误信息应清晰、包含上下文。
