不忘初心,砥砺前行

作者 | 陌无崖

转载请联系授权

Working with Errors in Go 1.13

Damien Neil and Jonathan Amsterdam

17 October 2019

介绍

在过去的十年中,Go将处理错误作为价值,已为我们服务良好。尽管标准库对错误的支持很简单(仅是errors.New和fmt.Errorf函数,它们产生的错误仅包含一条消息),但是内置的错误接口使Go程序员可以添加所需的任何信息。它所需要的只是一种实现Error方法的类型:

type QueryError struct {Query stringErr   error
}func (e *QueryError) Error() string { return e.Query + ": " + e.Err.Error() }

像这样的错误类型是普遍存在的,它们存储的信息,差异很大,从时间戳到文件,再到服务器,通常,该信息包括另一个较低级别的错误用来提供其他上下文。

在Go代码中,一个包含另一个错误的错误模式非常普遍,以至于经过广泛讨论,Go 1.13为其添加了明确的支持。这篇文章描述了标准库提供的支持:错误包中的三个新功能,以及fmt.Errorf的新格式动词。

在详细描述更改之前,让我们回顾一下在语言的早期版本中如何检查和构造错误。

Go 1.13之前的错误

检查错误

Go的错误是值,程序通过几种方式根据这些值作出决策,最常见的是将错误与nil进行比较,以查看操作是否失败。

if err != nil {// something went wrong
}

有时我们将错误与已知的标记值进行比较,以查看是否发生了特定错误。

var ErrNotFound = errors.New("not found")if err == ErrNotFound {// something wasn't found
}

错误值可以是满足语言定义的错误接口的任何类型。程序可以使用类型断言或类型开关将错误值视为更特定的类型。

type NotFoundError struct {Name string
}func (e *NotFoundError) Error() string { return e.Name + ": not found" }if e, ok := err.(*NotFoundError); ok {// e.Name wasn't found
}

添加信息

函数通常在向其添加信息时将错误传递给调用堆栈,例如对错误发生时所发生情况的简要描述。一种简单的方法是构造一个新错误,其中包括上一个错误:

if err != nil {return fmt.Errorf("decompress %v: %v", name, err)
}

使用fmt.Errorf创建新错误会丢弃原始错误中的所有内容(文本除外)。正如我们在QueryError上看到的那样,有时我们可能想要定义一个包含基础错误的新错误类型,并将其保留以供代码检查。再次是QueryError:

type QueryError struct {Query stringErr   error
}

程序可以查看*QueryError值,以根据潜在错误做出决策。有时您会看到称为“展开”错误的信息。

标准库中的os.PathError类型是一个错误的另一个示例,其中包含另一个错误。

if e, ok := err.(*QueryError); ok && e.Err == ErrPermission {// query failed because of a permission problem
}

Errors in Go 1.13

The Unwrap method

Go 1.13为错误和fmt标准库程序包引入了新功能,以简化处理包含其他错误的错误的过程。其中最重要的是约定,而不是更改:包含另一个错误的错误可能实现了Unwrap方法,该方法返回了基础错误。如果e1.Unwrap()返回e2,则说e1包装了e2,您可以将e1拆开以得到e2。

遵循此约定,我们可以将QueryError类型提供给Unwrap方法上方,该方法返回其包含的错误:

func (e *QueryError) Unwrap() error { return e.Err }

解包错误的结果本身可能具有Unwrap方法。我们称重复解开错误链产生的错误序列。

使用Is和As检查错误

Go 1.13错误程序包包括两个用于检查错误的新功能:Is和As。

errors.is函数将错误与值进行比较。

// Similar to:
//   if err == ErrNotFound { … }
if errors.Is(err, ErrNotFound) {// something wasn't found
}

As函数测试错误是否是特定类型。

// Similar to:
//   if e, ok := err.(*QueryError); ok { … }
var e *QueryError
if errors.As(err, &e) {// err is a *QueryError, and e is set to the error's value
}

在最简单的情况下,errors.Is函数的行为类似于对哨兵错误的比较,而errors.As函数的行为类似于类型声明。但是,在处理包装错误时,这些功能会考虑链中的所有错误。让我们从上方再次查看解开QueryError来检查基础错误的示例:

if e, ok := err.(*QueryError); ok && e.Err == ErrPermission {// query failed because of a permission problem
}

使用errors.is函数,我们可以这样写:

if errors.Is(err, ErrPermission) {// err, or some error that it wraps, is a permission problem
}

errors程序包还包括一个新的Unwrap函数,该函数返回调用错误的Unwrap方法的结果;如果错误没有Unwrap方法,则返回nil。通常,最好使用error.is或errors.As,因为这些函数将在单个调用中检查整个链。

Wrapping errors with %w

如前所述,通常使用fmt.Errorf函数将其他信息添加到错误中。

if err != nil {return fmt.Errorf("decompress %v: %v", name, err)
}

在Go 1.13中,fmt.Errorf函数支持新的%w动词。如果存在该动词,则fmt.Errorf返回的错误将具有Unwrap方法,该方法返回%w的参数,该参数必须是错误。在所有其他方面,%w与%v相同。

if err != nil {// Return an error which unwraps to err.return fmt.Errorf("decompress %v: %w", name, err)
}

用%w包裹一个错误使它可用于error.Is和errors.As:

err := fmt.Errorf("access denied: %w", ErrPermission)
...
if errors.Is(err, ErrPermission) ...

Whether to Wrap

使用fmt.Errorf或通过实现自定义类型向错误添加其他上下文时,您需要确定新错误是否应该包装原始错误。这个问题没有一个答案。它取决于创建新错误的上下文。包装错误以将其公开给调用者。这样做时请不要包装错误,以免暴露实现细节。

举一个例子,假设一个Parse函数从io.Reader读取一个复杂的数据结构。如果发生错误,我们希望报告发生错误的行号和列号。如果从io.Reader读取时发生错误,我们将要包装该错误以检查潜在问题。由于调用者向函数提供了io.Reader,因此暴露由它产生的错误是有意义的。

相反,对数据库进行多次调用的函数可能不应返回将这些调用之一的结果解开的错误。如果该函数使用的数据库是实现细节,那么暴露这些错误就是对抽象的违反。例如,如果软件包pkg的LookupUser函数使用Go的数据库/ sql软件包,则它可能会遇到sql.ErrNoRows错误。如果使用fmt.Errorf(“ accessing DB:%v”,err)返回该错误,则调用者无法在内部查找sql.ErrNoRows。但是,如果该函数返回fmt.Errorf(“ accessing DB:%w”,err),则调用者可以合理地编写

err := pkg.LookupUser(...)
if errors.Is(err, sql.ErrNoRows) …

此时,即使您不希望中断客户端,即使切换到其他数据库包,该函数也必须始终返回sql.ErrNoRows。换句话说,包装错误会使该错误成为您API的一部分。如果您不想将来将错误作为API的一部分来支持,则不应包装该错误。

请务必记住,无论是否换行,错误文本都将相同。试图理解该错误的人将以两种方式获得相同的信息。包装的选择是关于是否给程序提供更多信息,以便他们可以做出更明智的决定,还是保留该信息以保留抽象层。

使用Is和As方法自定义错误测试

errors.is函数检查链中的每个错误是否与目标值匹配。默认情况下,如果两者相等,则错误与目标匹配。此外,链中的错误可能会通过实现Is方法来声明它与目标匹配。

例如,考虑此错误是受Upspin错误包的启发,该错误包将错误与模板进行比较,仅考虑模板中非零的字段:

type Error struct {Path stringUser string
}func (e *Error) Is(target error) bool {t, ok := target.(*Error)if !ok {return false}return (e.Path == t.Path || t.Path == "") &&(e.User == t.User || t.User == "")
}if errors.Is(err, &Error{User: "someuser"}) {// err's User field is "someuser".
}

如果存在错误,则As函数会类似地查询As方法。

错误和包API

返回错误的程序包(大多数都会返回错误)应描述程序员可能依赖的那些错误的属性。一个经过精心设计的程序包也将避免返回带有不应依赖的属性的错误。

最简单的规范是说操作成功或失败,分别返回nil或non-nil错误值。在许多情况下,不需要进一步的信息。

如果我们希望函数返回可识别的错误条件,例如“未找到项目”,则可能会返回包装哨兵的错误。

var ErrNotFound = errors.New("not found")// FetchItem returns the named item.
//
// If no item with the name exists, FetchItem returns an error
// wrapping ErrNotFound.
func FetchItem(name string) (*Item, error) {if itemNotFound(name) {return nil, fmt.Errorf("%q: %w", name, ErrNotFound)}// ...
}

存在其他提供错误的模式,可以由调用方进行语义检查,例如直接返回哨兵值,特定类型或可以使用谓词函数检查的值。

在所有情况下,都应注意不要向用户公开内部细节。正如我们在上面的“是否要包装”中提到的那样,当您从另一个包中返回错误时,应该将错误转换为不暴露潜在错误的形式,除非您愿意将来再返回该特定错误 。

f, err := os.Open(filename)
if err != nil {// The *os.PathError returned by os.Open is an internal detail.// To avoid exposing it to the caller, repackage it as a new// error with the same text. We use the %v formatting verb, since// %w would permit the caller to unwrap the original *os.PathError.return fmt.Errorf("%v", err)
}

如果将函数定义为返回包装某些标记或类型的错误,请不要直接返回基础错误。

var ErrPermission = errors.New("permission denied")// DoSomething returns an error wrapping ErrPermission if the user
// does not have permission to do something.
func DoSomething() error {if !userHasPermission() {// If we return ErrPermission directly, callers might come// to depend on the exact error value, writing code like this:////     if err := pkg.DoSomething(); err == pkg.ErrPermission { … }//// This will cause problems if we want to add additional// context to the error in the future. To avoid this, we// return an error wrapping the sentinel so that users must// always unwrap it:////     if err := pkg.DoSomething(); errors.Is(err, pkg.ErrPermission) { ... }return fmt.Errorf("%w", ErrPermission)}// ...
}

结论

尽管我们讨论的更改仅包含三个功能和一个格式化动词,但我们希望它们对改善Go程序中错误处理的方式有很大帮助。我们希望通过包装来提供其他上下文将变得司空见惯,从而帮助程序做出更好的决策,并帮助程序员更快地发现错误。

正如Russ Cox在GopherCon 2019主题演讲中所说的那样,在Go 2的道路上,我们进行了实验,简化和发布。现在,我们已经发布了这些更改,我们期待接下来的实验。

本文为Golang官方博客部分文章的外文翻译,官方案例更加有料哦

查看完整源码可以点击阅读原文进入github仓库,如果喜欢,感谢你为我点一个星星^_^


END

今日推荐阅读

RabbitMQ系列笔记广播模式和路由模式 
RabbitMQ系列笔记入门篇

RabbitMQ系列笔记work模式

RabbitMQ系列笔记work模式

protoc语法详解及结合grpc定义服务

Golang中Model的使用

基于Nginx和Consul构建高可用及自动发现的Docker服务架构

▼关注我,一起成长

主要分享 学习心得、笔记、随笔▼

Working with Errors in Go 1.13相关推荐

  1. 06 Errors For Go1.13

    Errors before For 1.13 最简单的错误检查: if err != nil {// sth went wrong } 有事我们需要对sentinel error进行检查: var E ...

  2. Spring - Java/J2EE Application Framework 应用框架 第 13 章 集成表现层

    第 13 章 集成表现层 13.1. 简介 Spring之所以出色的一个原因就是将表现层从MVC的框架中分离出来.例如,通过配置就可以让Velocity或者XSLT来代替已经存在的JSP页面.本章介绍 ...

  3. java线程删除文件,线程“main”java.io.jgitinernalexception中的异常:无法删除临时文件c:\users\13 dec...

    我正在尝试使用jgit克隆git存储库. public static void main(String[] args) throws IOException, InvalidRemoteExcepti ...

  4. 13.SpringMVC核心技术-异常处理

    常用的SpringMVC异常处理方式主要是三种: 1.使用系统定义好的异常处理器   SimpleMappingExceptionResolver 2.使用自定义异常处理器 3.使用异常处理注解 Si ...

  5. Django中的Form

    2019独角兽企业重金招聘Python工程师标准>>> 一.使用Form Django中的Form使用时一般有两种功能: 1.生成html标签 2.验证输入内容 要想使用django ...

  6. 关于spring中commons-attributes-compiler.jar的使用问题

       昨天用spring做了个定时器,用于定时扫描某通讯公司外网ftp服务器的约定路径下是否有我需要的文件并下载到本公司服务器上.记得以前做过类似的一个定时器,觉得手到擒来的事情,没想到又折腾了大半天 ...

  7. 加法器的verilog实现(串行进位、并联、超前进位、流水线)

    总结: 从下面的Timing summary来看,流水线的频率最高.并行加法器次之,串行进位加法器再次,超前进位加法器最慢. 按理论,超前进位加法器应该比串行进位加法器快,此处为何出现这种情况,原因未 ...

  8. linux下ip命令用法

    配置数据转发,可以通过 1.路由转发即用用路由器实现: 2.使用NAT转发: 简单的说: 路由表内的信息只是指定数据包在路由器内的下一个去处.并不能改变数据包本身的地址信息.即它只是"换条路 ...

  9. [转载] python之路《第二篇》Python基本数据类型

    参考链接: Python中的Inplace运算符| 1(iadd(),isub(),iconcat()-) 运算符 1.算数运算: 2.比较运算: 3.赋值运算: 4.逻辑运算: 5.成员运算: 6. ...

最新文章

  1. Linux字符驱动中动态分配设备号与动态生成设备节点
  2. 一个封装的使用Apache HttpClient进行Http请求(GET、POST、PUT等)的类。
  3. 手撕设计模式之「工厂方法模式」(Java描述)
  4. 轻量级定时任务框架:APScheduler
  5. 【原】网页程序学习Linux利器-----jsuix
  6. 文件上传与下载问题记录
  7. PHP的几种排序算法的比较
  8. python xlrd读取excel慢_python操作Excel读写--使用xlrd
  9. php7链接数据库报错The server requested authentication method unknown to the client
  10. 面向未来 “亿”触即发-中科曙光技术创新大会重磅发布多项创新举措与成果...
  11. 数据:以太坊2.0合约质押新增7.47万ETH
  12. http://dev2dev.bea.com.cn/bbs/thread.jspa?forumID=122threadID=9172tstart=0
  13. 【黑马程序员西安中心】一个内向青年的转变
  14. 计算机运行大型游戏特热,玩大型游戏cpu温度多少度算正常
  15. 郭天祥 10天搞定单片机 (3)数码管+中断
  16. 259-数据明文传输的安全问题
  17. 阿里云AI训练营SQL入门到实践 Task3:视图、子查询、函数等
  18. import keras时遇到的错误 TypeError: Descriptors cannot not be created directly. If this call came from a _
  19. OGG 抓取进程模式转换(集成模式→经典模式)(integrated→classic)
  20. 华为p10 android几,华为p10国行版和海外版有什么区别 配置参数对比评测

热门文章

  1. Android各厂商自启动管理开发
  2. (转载)libvirt 问题解决记录集
  3. MySQL的InnoDB存储引擎中,缓冲池中的Changer Buffer与系统表空间中的Changer Buffer的关系
  4. 给大家整理了几个好用的远程软件真实测评,大学生和打工人必备~用好远程,效率翻倍【建议收藏】
  5. 【大数据】季节性模型概述
  6. 细分市场——电视重生 | 《商业价值》杂志
  7. 什么是继承 继承的好处
  8. 小程序自定义导航栏搜索和自定义底部tab(动态切换)
  9. win7 linux终端模拟器,SecureCRT(终端仿真器)
  10. python怎么分行读取txt文件_python怎么读取txt文件内容