一文搞懂Go错误链

news2024/10/5 13:16:01

0. Go错误处理简要回顾

Go是一种非常强调错误处理的编程语言。在Go中,错误被表示为实现了error接口的类型的值,error接口只有一个方法:

type error interface {
 Error() string
}

这个接口的引入使得Go程序可以以一致和符合惯用法的方式进行错误处理。

在所有编程语言中,错误处理的挑战之一都是能提供足够的错误上下文信息,以帮助开发人员诊断问题,同时又可以避免开发人员淹没在不必要的细节中。在Go中,这一挑战目前是通过使用**错误链(error chain)**来解决的。

注:Go官方用户调查结果[1]表明,Go社区对Go错误处理机制改进的期望还是很高的。这对Go核心团队而言,依然是一个不小的挑战。好在Go 1.18泛型落地[2],随着Go泛型的逐渐成熟,更优雅的错误处理方案有可能会在不远的将来浮出水面。

错误链是一种将一个错误包裹在另一个错误中的技术,以提供关于错误的额外的上下文。当错误通过多层代码传播时,这种技术特别有用,每层代码都会为错误信息添加自己的上下文。

不过,最初Go的错误处理机制是不支持错误链的,Go对错误链的支持和完善是在Go 1.13版本[3]中才开始的事情。

众所周知,在Go中,错误处理通常使用if err != nil的惯用法来完成。当一个函数返回一个错误时,调用代码会检查该错误是否为nil。如果错误不是nil,通常会被打印到日志中或返回给调用者。

例如,看下面这个读取文件的函数:

func readFile(filename string) ([]byte, error) {
    data, err := os.ReadFile(filename)
    if err != nil {
        return nil, err
    }
    return data, nil
}

在这段代码中,os.ReadFile()如果读取文件失败,会返回一个错误。如果发生这种情况,readFile函数会将错误返回给它的调用者。Go的这种基本的错误处理机制简单有效好理解,但它也有自己的局限性。其中一个主要的限制是错误信息可能是模糊的。当一个错误在多层代码中传播时,开发人员可能很难确定错误的真实来源和原因。 我们看一下下面这段代码:

func processFile(filename string) error {
    data, err := readFile(filename)
    if err != nil {
        return err
    }
    // process the file data...
    return nil
}

在这个例子中,如果readFile()返回一个错误,错误信息将只表明该文件无法被读取,它不会提供任何关于造成错误的原因或错误发生地点的准确信息

Go基本错误处理的另一个约束是在处理错误时,错误的上下文可能会丢失。尤其是当一个错误通过多层代码时,某一层可能会忽略收到的错误信息,而是构造自己的错误信息并返回给调用者,这样最初的错误上下文就会在错误的传递过程中丢失了,这不利于问题的快速诊断。

那么,我们如何解决这些限制呢?下面我们就来探讨一下错误链是如何如何帮助Go开发人员解决这些限制问题的。

1. 错误包装(error wrapping)与错误链

为了解决基本错误处理的局限性,Go在1.13版本中提供了Unwrap接口和fmt.Errorf的%w的格式化动词[4],用于构建可以包裹(wrap)其他错误的错误以及从一个包裹了其他错误的错误中判断是否有某个指定错误,并从中提取错误信息。

fmt.Errorf是最常用的用于包裹错误的函数,它接收一个现有的错误,并将其包装在一个新的错误中,并可以附着更多的错误上下文信息。

例如,改造一下上面的示例代码:

func processFile(filename string) error {
    data, err := readFile(filename)
    if err != nil {
        return fmt.Errorf("failed to read file: %w", err)
    }
    // process the file data...
    return nil
}

在这段代码中,fmt.Errorf通过%w创建了一个新的错误,新错误包裹(wrap)了原来的错误,并附加了一些错误上下文信息(failed to read file)。这个新的错误可以在调用堆栈中传播并提供更多关于这个错误的上下文。

为了从错误链中检索原始错误,Go在errors包中提供了Is、As和Unwrap()函数。Is和As函数用于判定某个error是否存在于错误链中,Unwrap这个函数返回错误链中的下一个直接错误。

下面是一个完整的例子:

func readFile(filename string) ([]byte, error) {
    data, err := os.ReadFile(filename)
    if err != nil {
        return nil, err
    }
    return data, nil
}

func processFile(filename string) error {
    data, err := readFile(filename)
    if err != nil {
        return fmt.Errorf("failed to read file: %w", err)
    }
    fmt.Println(string(data))
    return nil
}

func main() {
    err := processFile("1.txt")
    if err != nil {
        fmt.Println(err)
        fmt.Println(errors.Is(err, os.ErrNotExist))
        err = errors.Unwrap(err)
        fmt.Println(err)
        err = errors.Unwrap(err)
        fmt.Println(err)
        return
    }
}

运行这个程序(前提:1.txt文件并不存在),结果如下:

$go run demo1.go
failed to read file: open 1.txt: no such file or directory
true
open 1.txt: no such file or directory
no such file or directory

该示例中错误的wrap和unwrap关系如下图:

bcc09b701cbc86fc3ec6fc6cff7648fb.png

像这种由错误逐个包裹而形成的链式结构(如下图),我们称之为错误链

5bad8b6cbcdab504180a7982350cfe8c.png

接下来,我们再来详细说一下Go错误链的使用。

2. Go中错误链的使用

2.1 如何创建错误链

就像前面提到的,我们通过包裹错误来创建错误链

目前Go标准库中提供的用于wrap error的API有fmt.Errorf和errors.Join。fmt.Errorf最常用,在上面的示例中我们演示过了。errors.Join用于将一组errors wrap为一个error。

fmt.Errorf也支持通过多个%w一次打包多个error,下面是一个完整的例子:

func main() {
    err1 := errors.New("error1")
    err2 := errors.New("error2")
    err3 := errors.New("error3")

    err := fmt.Errorf("wrap multiple error: %w, %w, %w", err1, err2, err3)
    fmt.Println(err)
    e, ok := err.(interface{ Unwrap() []error })
    if !ok {
        fmt.Println("not imple Unwrap []error")
        return
    }
    fmt.Println(e.Unwrap())
}

示例运行输出如下:

wrap multiple error: error1, error2, error3
[error1 error2 error3]

我们看到,通过fmt.Errorf一次wrap的多个error在String化后,是在一行输出的。这点与errors.Join的有所不同。下面是用errors.Join一次打包多个error的示例:

func main() {
    err1 := errors.New("error1")
    err2 := errors.New("error2")
    err3 := errors.New("error3")

    err := errors.Join(err1, err2, err3)
    fmt.Println(err)
    errs, ok := err.(interface{ Unwrap() []error })
    if !ok {
        fmt.Println("not imple Unwrap []error")
        return
    }
    fmt.Println(errs.Unwrap())
}

这个示例输出如下:

$go run demo2.go
error1
error2
error3
[error1 error2 error3]

我们看到,通过errors.Join一次wrap的多个error在String化后,每个错误单独占一行。

如果对上面的输出格式都不满意,那么你还可以自定义Error类型,只要至少实现了String() string和Unwrap() error 或Unwrap() []error即可。

2.2 判定某个错误是否在错误链中

前面提到过errors包提供了Is和As函数来判断某个错误是否在错误链中,对于一次wrap多个error值的情况,errors.Is和As也都按预期可用。

2.3 获取错误链中特定错误的上下文信息

有些时候,我们需要从错误链上获取某个特定错误的上下文信息,通过Go标准库可以至少有两种实现方式:

第一种:通过errors.Unwrap函数来逐一unwrap错误链中的错误。

由于不确定错误链上的error个数以及每个error的特征,这种方式十分适合用来获取root cause error,即错误链中最后面的一个error。下面是一个示例:

func rootCause(err error) error {
    for {
        e, ok := err.(interface{ Unwrap() error })
        if !ok {
            return err
        }
        err = e.Unwrap()
        if err == nil {
            return nil
        }
    }
}

func main() {
    err1 := errors.New("error1")

    err2 := fmt.Errorf("2nd err: %w", err1)
    err3 := fmt.Errorf("3rd err: %w", err2)

    fmt.Println(err3) // 3rd err: 2nd err: error1

    fmt.Println(rootCause(err1)) // error1
    fmt.Println(rootCause(err2)) // error1
    fmt.Println(rootCause(err3)) // error1
}

第二种:通过errors.As函数将error chain中特定类型的error提取出来

error.As函数用于判断某个error是否是特定类型的error,如果是则将那个error提取出来,比如:

type MyError struct {
    err string
}

func (e *MyError) Error() string {
    return e.err
}

func main() {
    err1 := &MyError{"temp error"}
    err2 := fmt.Errorf("2nd err: %w", err1)
    err3 := fmt.Errorf("3rd err: %w", err2)

    fmt.Println(err3)

    var e *MyError
    ok := errors.As(err3, &e)
    if ok {
        fmt.Println(e)
        return
    }
}

在这个示例中,我们通过errors.As将错误链err3中的err1提取到e中,后续就可以使用err1这个特定错误的信息了。

3. 小结

错误链是Go中提供信息丰富的错误信息的一项重要技术。通过用额外的上下文包装错误,你可以提供关于错误的更具体的信息,并帮助开发人员更快地诊断出问题。

不过错误链在使用中有一些事项还是要注意的,比如:避免嵌套错误链。嵌套的错误链会使你的代码难以调试,也难以理解错误的根本原因。

结合错误链,通过给错误添加上下文,创建自定义错误类型,并在适当的抽象层次上处理错误,你可以写出简洁、可读和信息丰富的错误处理代码。


“Gopher部落”知识星球[5]旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2023年,Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码,关注代码质量并深入理解Go核心技术,并继续加强与星友的互动。欢迎大家加入!

43695cb5b67984eb9dff00c4c0a0e78e.jpegf95dabbb3418d4d327f14a9598e3a939.png

26e27cfd81ac39e336ea98e1c6716a3e.pngf44e49a516bc5f2e99a4336e57a4d19f.jpeg

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址[6]:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 - https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx

  • 微博2:https://weibo.com/u/6484441286

  • 博客:tonybai.com

  • github: https://github.com/bigwhite

916673885ee8d06d46e7c145018e95eb.jpeg

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

参考资料

[1] 

Go官方用户调查结果: https://go.dev/blog/survey2022-q2-results

[2] 

Go 1.18泛型落地: https://tonybai.com/2022/04/20/some-changes-in-go-1-18

[3] 

Go 1.13版本: https://tonybai.com/2019/10/27/some-changes-in-go-1-13/

[4] 

Go在1.13版本中提供了Unwrap接口和fmt.Errorf的%w的格式化动词: https://tonybai.com/2019/10/18/errors-handling-in-go-1-13/

[5] 

“Gopher部落”知识星球: https://wx.zsxq.com/dweb2/index/group/51284458844544

[6] 

链接地址: https://m.do.co/c/bff6eed92687

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/526884.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python实现哈里斯鹰优化算法(HHO)优化LightGBM回归模型(LGBMRegressor算法)项目实战

说明:这是一个机器学习实战项目(附带数据代码文档视频讲解),如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 2019年Heidari等人提出哈里斯鹰优化算法(Harris Hawk Optimization, HHO),该算法有较强的全…

有主题的图文内容创作 | AIGC实践

话说,昨天我发布了第一篇,内容由ChatGPT和Midjourney协助完成的文章:胡同与侏罗纪公园的时空交错 | 胡同幻想 在这篇文章中,大约70%图文内容由ChatGPT和Midjourney输出。我个人参与的部分,主要是提出指令(P…

Monaco Editor编辑器教程(三一):在编辑器中实现模拟调试的交互

前言 最近有小伙伴咨询如何在编辑中实现 像vscode调试代码那样,可以打断点,可以高亮当前运行的一行。这样的需求并不多见,如果要做,那肯定是对编辑器做一个深层次的定制。一般很少很少会实现这种在浏览器中调试。 目前我还没见过,如果有遇到过的朋友可以指点一下。我去学…

Cesium AI GPT 文档 源码 ChatGPT问答

我用Cesium104.0的 源码 | 文档 | 3DTiles标准 作为上下文语料定制了一个智能Cesium专家问答助手 语料: 3D Tiles Specificationhttps://cesium.com/downloads/cesiumjs/releases/1.104/Build/CesiumUnminified/Cesium.jshttps://github1s.com/CesiumGS/cesium/blob/HEAD/Doc…

redis中的管道

Redis 管道 文章目录 1. 前言2. Redis 管道3. 小总结 1. 前言 通过一个问题引出 我们接下来要学习的 Redis 管道 : 提问 : 如何优化频繁命令往返造成的性能瓶颈 ? 另外 : 关于上面这个问题的由来 也可以简单的说一说 上面所说的思路 其实就是管道的概念 &#xff0…

读俞敏洪的书

没有认真写过一篇关于书籍的读后感文章,但在读完俞敏洪老师这本书后,想推荐给大家,也想分享下我的想法。 几周前,我在微信读书首页看到了俞敏洪老师的读书推荐 《在绝望中寻找希望》——俞敏洪写给迷茫不安的年轻人 有好几个晚上&…

电池只能充电500次?别太荒谬!收下这份真正的充电秘籍

我们的生活已经离不开电子设备了,而电子设备嘛,又离不开给它们提供能源的电池。在网上有许许多多的“延长电池寿命小技巧”,比如“新买的电子设备,第一次充电之前要把电都放完”“笔记本电脑一直插着电源可以保护电池”“长期不用…

区分COCO数据集的coco minival和coco test-dev、conda常用命令和python -m 的作用

1、COCO数据集的测试集coco minival和coco test-dev: 两个数据集在官方网站对应的内容如下所示: COCO数据集官网:https://cocodataset.org/#download 两个数据集的区分参考网址:https://zhuanlan.zhihu.com/p/533676547 2、conda常用命令…

linux中epoll+socket实战

目录 参考前言案例 一、epoll的基本使用首先是epoll_create函数:然后是epoll_ctl函数:最后是epoll_wait函数:关于ET(边沿触发)、LT(水平触发)两种工作模式可以得出这样的结论: 二、使用代码简易…

基于html+css的图展示71

准备项目 项目开发工具 Visual Studio Code 1.44.2 版本: 1.44.2 提交: ff915844119ce9485abfe8aa9076ec76b5300ddd 日期: 2020-04-16T16:36:23.138Z Electron: 7.1.11 Chrome: 78.0.3904.130 Node.js: 12.8.1 V8: 7.8.279.23-electron.0 OS: Windows_NT x64 10.0.19044 项目…

某音X-Bogus算法研究 2023-05-15

本文以教学为基准,研究JavaScript算法及反爬策略、本文提供的可操作性不得用于任何商业用途和违法违规场景。 如有侵权,请联系我进行删除。 今天我们分析一下douyin个人主页数据获取。 大多数小伙伴应该都知道想要拿到douyin的数据也不是那么容易的&a…

近世代数 笔记与题型连载 第十三章(环与域)

文章目录 基本概念1.环1.1.环的定义1.2.环的性质1.3.几种特殊的环1.4.子环 2.域2.1.域的定义2.2.环与域的同态 相关题型1.验证一个代数系统是否是一个环2.判断一个代数系统是否是整环3.判断一个代数系统是否是另一个代数系统的子环4.判断一个代数系统是否是域 基本概念 1.环 …

sed编辑器命令

sed编辑器 sed是一种流编辑器,流编辑器会在编辑器处理数据之前基于预先提供的一组规则来编辑数据流。 sed编辑器可以根据命令来处理数据流中的数据,这些命令要么从命令行中输入,要么存储在一个命令文本文件中。 sed 的工作流程主要包括读取…

Nat. Commun | 中国海洋大学张伟鹏组揭示海洋生物被膜群落硫氧化主要菌群及其作用机制...

海洋生物被膜玫瑰杆菌的厌氧硫氧化机制 Anaerobic thiosulfate oxidation by the Roseobacter group is prevalent in marine biofilms Article,2022-04-11,Nature Communications,[IF 17.7] DOI:10.1038/s41467-023-37759-4 原文…

Apache Hive函数高阶应用、性能调优

Hive的函数高阶应用 explode函数 explode属于UDTF函数,表生成函数,输入一行数据输出多行数据。 功能: explode() takes in an array (or a map) as an input and outputs the elements of the array (map) as separate rows.--explode接收…

「 操作系统 」CPU缓存一致性协议MESI详解

「 操作系统 」CPU缓存一致性协议MESI详解 参考&鸣谢 缓存一致性协议MESI 小天 CPU缓存一致性协议MESI 枫飘雪落 CPU缓存一致性协议(MESI) 广秀 2.4 CPU 缓存一致性 xiaoLinCoding 文章目录 「 操作系统 」CPU缓存一致性协议MESI详解一、计算机的缓存一致性二、CPU高速缓存…

100种思维模型之长远思考思维模型-63

古语有云:“人无远虑,必有近忧!” 任正非说:不谋长远者,不足以谋一时! 长远思考思维,一个提醒我们要运用长远眼光,树立宏大目标,关注长期利益的思维模型 01何谓长远思考…

深度学习架构的对比分析

深度学习的概念源于人工神经网络的研究,含有多个隐藏层的多层感知器是一种深度学习结构。深度学习通过组合低层特征形成更加抽象的高层表示,以表征数据的类别或特征。它能够发现数据的分布式特征表示。深度学习是机器学习的一种,而机器学习是…

浅谈数据资产测绘系统的作用和挑战

随着数据被定义为第五大生产要素,数据已经成为数字经济发展的核心驱动力。数据资源的充分利用和开放共享给政企单位带来便利的同时,也带来了相应的数据安全风险。因此,摸清并动态掌握数据资产情况,持续进行数据资产测绘就成为企业…

Golang每日一练(leetDay0066) 有效电话号码、转置文件

目录 193. 有效电话号码 Valid Phone Numbers 🌟 194. 转置文件 Transpose File 🌟🌟 🌟 每日一练刷题专栏 🌟 Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每日一练 专栏 193. 有效电话号…