当前位置: 首页 > news >正文

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 保留原始错误。

  • 错误信息应清晰、包含上下文。

http://www.dtcms.com/a/557196.html

相关文章:

  • Go语言中通过get请求获取api.open-meteo.com网站的天气数据
  • 哪些网站可以做微课技术支持 英铭网站建设
  • STM32位带操作理论实践
  • 住房和城乡建设部的网站首页不懂网站怎么做平台
  • 禁止Windows 10升级至Windows 11的方法
  • 人工智能之数学基础:随机变量函数的分布(离散和连续)
  • 30.16.2.表现层框架设计
  • DMS 迁移错误:String Length Exceeds DDL Length 完整解决方案
  • 福建建设厅网站官网宣传推广方案
  • 网站搭建的步骤百度网站怎样做
  • 网站的建设与开发discover wordpress
  • apk反编译修改教程系列-----读懂 Android 签名机制:从 V1 到 V4的签名区别
  • 人工智能本体论!
  • 将Git项目的所有远程分支打包成压缩包文件
  • 做液压的公司网站佛山网站建设格式有哪些
  • 深圳做微商网站的公司二维码生成器app
  • WebClient发送请求示例
  • Wireshark TS | 接收数据超出接收窗口续
  • mapset的使用
  • 要事优先-深耕目标
  • 禄劝彝族苗族网站建设食品 技术支持 东莞网站建设
  • 宁波市省网站建设济南工程建设交易信息网
  • 伯克利哈斯商学院的金融工程硕士(MFE)
  • 政安晨【零基础玩转开源AI项目】video-subtitle-remover 去除视频字幕水印(图像也可以)(基于Ubuntu Linux系统)
  • 温州市名城建设集团有限公司网站二级域名如何申请
  • 【C++】模拟算法习题
  • QLoRA基础知识和微调原理学习
  • 在 vscode 中配置juypter notebook 插件
  • 石家庄好用的招聘网站门户网站网站建设
  • ENERGY Designer:重构跨平台GUI开发的高效解决方案