Go 学习笔记(64)— Go error.New 创建接口错误对象、fmt.Errorf 创建接口错误对象、errors.Is 和 errors.As
1. error 接口定义
除用 panic
引发中断性错误外,还可返回 error
类型错误对象来表示函数调用状态。error
接口是 Go
原生内置的类型,它的定义如下:
// $GOROOT/src/builtin/builtin.go
type interface error {Error() string
}
在这个接口类型的声明中只包含了一个方法 Error
。Error
方法不接受任何参数,但是会返回一个 string
类型的结果。它的作用是返回错误信息的字符串表示形式。
任何实现了 error
的 Error
方法的类型的实例,都可以作为错误值赋值给 error
接口变量。
一般情况下在 Go
里只使用 error
类型判断错误, Go
官方希望开发者能够很清楚的掌控所有的异常,在每一个可能出现异常的地方都返回或判断 error
是否存在。
标准库 errors.New
和 fmt.Errorf
函数用于创建实现 error
接口的错误对象。通过判断错误对象实例来确定具体错误类型。
err := errors.New("your first demo error")
errWithCtx = fmt.Errorf("index %d is out of bounds", i)
这两种方法实际上返回的是同一个实现了 error
接口的类型的实例,这个未导出的类型就是errors.errorString
,它的定义是这样的:
// $GOROOT/src/errors/errors.go
type errorString struct {s string
}func (e *errorString) Error() string {return e.s
}
2. 创建 error 接口对象
2.1 errors.New
创建 error
接口错误对象
在生成 error
类型值的时候,用到了 errors.New
函数。
这是一种最基本的生成错误值的方式。我们调用它的时候传入一个由字符串代表的错误信息,它会给返回给我们一个包含了这个错误信息的 error
类型值。
该值的静态类型当然是 error
,而动态类型则是一个在 errors
包中的,包级私有的类型 *errorString
。
package mainimport "errors"var ErrDivByZero = errors.New("division by zero")func div(x, y int) (int, error) {if y == 0 {return 0, ErrDivByZero}return x / y, nil
}
func main() {switch z, err := div(10, 0); err {case nil:println(z)case ErrDivByZero:panic(err)}
}
输出结果:
panic: division by zerogoroutine 1 [running]:
main.main()/home/wohu/gocode/src/hello.go:18 +0xa7
exit status 2
2.2 fmt.Errorf
创建 error
接口错误对象
我们已经知道,通过调用 fmt.Printf
函数,并给定占位符 %s
就可以打印出某个值的字符串表示形式。对于其他类型的值来说,只要我们能为这个类型编写一个 String
方法,就可以自定义它的字符串表示形式。
而对于 error
类型值,它的字符串表示形式则取决于它的 Error
方法。在上述情况下,fmt.Printf
函数如果发现被打印的值是一个 error
类型的值,那么就会去调用它的 Error
方法。fmt
包中的这类打印函数其实都是这么做的。
顺便提一句,当我们想通过模板化的方式生成错误信息,并得到错误值时,可以使用 fmt.Errorf
函数。该函数所做的其实就是先调用 fmt.Sprintf
函数,得到确切的错误信息;再调用 errors.New
函数,得到包含该错误信息的 error
类型值,最后返回该值。
package mainimport ("errors""fmt"
)func main() {err1 := fmt.Errorf("invalid contents: %s", "#$%")err2 := errors.New(fmt.Sprintf("invalid contents: %s", "#$%"))if err1.Error() == err2.Error() {fmt.Println("The error messages in err1 and err2 are the same.")}
}
输出结果:
The error messages in err1 and err2 are the same.
fmt.Errorf
函数 示例:
package mainimport "fmt"func foo(i int, j int) (r int, err error) {if j == 0 {err = fmt.Errorf("参数 2 不能为 %d", j) //给 err 变量赋值一个 error 对象return //返回 r 和 err,因为定义了返回值变量名,所以不需要在这里写返回变量}return i / j, err //如果没有赋值 error 给 err 变量,err 是 nil
}func main() {//传递 add 函数和两个数字,计算相加结果n, err := foo(100, 0)if err != nil { //判断返回的 err 变量是否为 nil,如果不是,说明函数调用出错,打印错误内容println(err.Error())} else {println(n)}
}
3. 可导出哨兵错误值方式
Go
标准库采用了定义导出的“哨兵”错误值的方式,来辅助错误处理方检视错误值并做出错误处理分支的决策,比如下面的 bufio
包中定义的“哨兵错误”:
// $GOROOT/src/bufio/bufio.go
var (ErrInvalidUnreadByte = errors.New("bufio: invalid use of UnreadByte")ErrInvalidUnreadRune = errors.New("bufio: invalid use of UnreadRune")ErrBufferFull = errors.New("bufio: buffer full")ErrNegativeCount = errors.New("bufio: negative count")
)
下面的代码片段利用了上面的哨兵错误,进行错误处理分支的决策:
data, err := b.Peek(1)
if err != nil {switch err {case bufio.ErrNegativeCount:// ... ...returncase bufio.ErrBufferFull:// ... ...returncase bufio.ErrInvalidUnreadByte:// ... ...returndefault:// ... ...return}
}
一般“哨兵”错误值变量以 ErrXXX
格式命名。
从 Go 1.13
版本开始,标准库 errors
包提供了 Is
函数用于错误处理方对错误值的检视。Is
函数类似于把一个 error
类型变量与“哨兵”错误值进行比较,比如下面代码:
// 类似 if err == ErrOutOfBounds{ … }
if errors.Is(err, ErrOutOfBounds) {// 越界的错误处理
}
不同的是,如果 error
类型变量的底层错误值是一个包装错误(Wrapped Error),errors.Is
方法会沿着该包装错误所在错误链(Error Chain),与链上所有被包装的错误(Wrapped Error)进行比较,直至找到一个匹配的错误为止。下面是 Is
函数应用的一个例子:
var ErrSentinel = errors.New("the underlying sentinel error")func main() {err1 := fmt.Errorf("wrap sentinel: %w", ErrSentinel)err2 := fmt.Errorf("wrap err1: %w", err1)println(err2 == ErrSentinel) //falseif errors.Is(err2, ErrSentinel) {println("err2 is ErrSentinel")return}println("err2 is not ErrSentinel")
}
在这个例子中,我们通过 fmt.Errorf
函数,并且使用 %w
创建包装错误变量 err1
和 err2
,其中 err1
实现了对 ErrSentinel
这个“哨兵错误值”的包装,而 err2
又对 err1
进行了包装,这样就形成了一条错误链。位于错误链最上层的是 err2
,位于最底层的是 ErrSentinel
。之后,我们再分别通过值比较和 errors.Is
这两种方法,判断 err2
与 ErrSentinel
的关系。运行上述代码,我们会看到如下结果:
false
err2 is ErrSentinel
我们看到,通过比较操作符对 err2
与 ErrSentinel
进行比较后,我们发现这二者并不相同。而 errors.Is
函数则会沿着 err2
所在错误链,向下找到被包装到最底层的“哨兵”错误值 ErrSentinel
。
所以,如果你使用的是 Go 1.13
及后续版本,我建议你尽量使用 errors.Is
方法去检视某个错误值是否就是某个预期错误值,或者包装了某个特定的“哨兵”错误值。
4. 错误值类型检视策略
上面基于 Go
标准库提供的错误值构造方法构造的“哨兵”错误值,除了让错误处理方可以“有的放矢”的进行值比较之外,并没有提供其他有效的错误上下文信息。那如果遇到错误处理方需要错误值提供更多的“错误上下文”的情况,上面这些错误处理策略和错误值构造方式都无法满足。
这种情况下,我们需要通过自定义错误类型的构造错误值的方式,来提供更多的“错误上下文”信息。并且,由于错误值都通过 error
接口变量统一呈现,要得到底层错误类型携带的错误上下文信息,错误处理方需要使用 Go
提供的类型断言机制(Type Assertion
)或类型选择机制(Type Switch
),这种错误处理方式,我称之为错误值类型检视策略。
从 Go 1.13
版本开始,标准库 errors
包提供了As
函数给错误处理方检视错误值。As
函数类似于通过类型断言判断一个 error
类型变量是否为特定的自定义错误类型,如下面代码所示:
// 类似 if e, ok := err.(*MyError); ok { … }
var e *MyError
if errors.As(err, &e) {// 如果err类型为*MyError,变量e将被设置为对应的错误值
}
不同的是,如果 error
类型变量的动态错误值是一个包装错误,errors.As
函数会沿着该包装错误所在错误链,与链上所有被包装的错误的类型进行比较,直至找到一个匹配的错误类型,就像 errors.Is
函数那样。下面是 As
函数应用的一个例子:
type MyError struct {e string
}func (e *MyError) Error() string {return e.e
}func main() {var err = &MyError{"MyError error demo"}err1 := fmt.Errorf("wrap err: %w", err)err2 := fmt.Errorf("wrap err1: %w", err1)var e *MyErrorif errors.As(err2, &e) {println("MyError is on the chain of err2")println(e == err) return } println("MyError is not on the chain of err2")
}
运行上述代码会得到:
MyError is on the chain of err2
true
我们看到,errors.As
函数沿着 err2
所在错误链向下找到了被包装到最深处的错误值,并将 err2
与其类型 * MyError
成功匹配。匹配成功后,errors.As
会将匹配到的错误值存储到 As
函数的第二个参数中,这也是为什么 println(e == err)
输出 true
的原因。
所以,如果你使用的是 Go 1.13
及后续版本,请尽量使用 errors.As
方法去检视某个错误值是否是某自定义错误类型的实例。
Go 学习笔记(64)— Go error.New 创建接口错误对象、fmt.Errorf 创建接口错误对象、errors.Is 和 errors.As相关推荐
- Linux从头开始学--学习笔记9知识点补充-ubuntu,centos;在linux上创建c程序;linux基础命令,shell命令,vi命令,man帮助手册
这是我从头开始学习Linux的学习笔记,后续还会更新. 记录自己的技术成长,也希望和大家分享交流,欢迎关注~ 本笔记为coursera网站课程<Linux for Developers>的 ...
- 【极客学院】-python学习笔记-Python快速入门(面向对象-引入外部文件-Web2Py创建网站)
极客学院的课程,感觉很有意思,每节课都很短,但是很干货,我喜欢这个节奏 http://www.jikexueyuan.com/course/203.html 课程背景: Python语言功能强大, 能 ...
- Vue学习笔记(三)Vue2三种slot插槽的概念与运用 | ES6 对象的解构赋值 | 基于Vue2使用axios发送请求实现GitHub案例 | 浏览器跨域问题与解决
文章目录 一.参考资料 二.运行环境 三.Vue2插槽 3.1 默认插槽 3.2 具名插槽 3.3 作用域插槽 ES6解构赋值概念 & 作用域插槽的解构赋值 3.4 动态插槽名 四.GitHu ...
- Linux shell 学习笔记(11)— 理解输入和输出(标准输入、输出、错误以及临时重定向和永久重定向)
1. 理解输入和输出 1.1 标准文件描述符 Linux 系统将每个对象当作文件处理.这包括输入和输出进程.Linux 用文件描述符(file descriptor)来标识每个文件对象.文件描述符是一 ...
- python学习笔记(六)——类的初始化(__init__)、类属性和类方法 和 对象
学习本篇文章后会了解到:类的创建,为对象添加属性,对象的初始化,自定义对象的输出,类属性和类方法的创建. 1. 类的定义与格式 类是对一群具有相同特征或者行为的事物的一个统称. 类是一个模块,是负责创 ...
- Java面向对象基础学习笔记(构造、重载、继承、多态、抽象类、接口、模块)
Java面向对象编程包含哪些内容? 怎么理解面向对象编程? 现实生活中,我们定义了"人"的抽象概念,这就是类class,生活中的每一个具体的人就是实例instance. class ...
- oracle学习日志---返回RemoteOperationException: ERROR: Wrong password for user-错误的用户名密码-的错误的解决办法...
ERROR Wrong password for user 在OEM中有些操作需要输入操作系统的用户名密码才能继续下去,但是以前无论怎样输入,总是会返回RemoteOperationException ...
- unity学习笔记-番外(接入百度和轻语的AI智能接口实现语音识别和语音播放)
接入百度和轻语的AI智能接口实现语音识别和语音播放 语音识别 思路 代码 语音合成 思路 总结 语音识别 思路 先在百度和轻语申请接口,获得appkey和secretkey(这是为了获得鉴权,也就是t ...
- 深入浅出mfc学习笔记——六大关键技术之仿真_运行时和动态创建
1:PS88:MFC的类层次结构 <1>CObject <2>CCmdTarget,CDocument <3>CCmdTarget_CWinThread_CWinA ...
最新文章
- 第四周项目四-程序分析(1)
- MySql各引擎特点和性能测试
- 语言设计谁年龄大_这桌子谁设计的?脑洞够大,除能360°翻转,打台球,乒乓球都行...
- 钮扣电池电压电量_纽扣厂
- java如何实取随机数_java - 如何在Kotlin中获取随机数?
- 【git】gitk 通过图形界面工具来查看版本历史
- 中国如何在 AI 芯片实现弯道超车?
- java 广告插件_徒手创建一个chrome扩展-屏蔽广告插件
- 专访UCloud徐亮:UCloud虚拟网络的演进之路
- react 加粗_css字体如何加粗?
- 3D打印技术到底有多强大?
- 能打开网页 玩游戏找不到服务器,浏览器打不开网页但可以玩游戏上QQ?原是DNS在作怪...
- 论劳动生产力进步的原因,兼论劳动产品在不同阶级人民之间自然分配顺序(读后感)
- 华为云nbiot接入示例_nbiot
- JSP+MYSQL网上作业提交及管理系统
- [附源码]Python计算机毕业设计Django的项目管理系统
- 用起这 16 个顶尖 Vue 开源项目,节约更多的时间摸鱼
- <Python的输入、输出、注释>——《Python》
- 最长对称字符串php_PHP-字符串过长不用担心
- 【OpenCV入门教程之十二】OpenCV边缘检测:Canny算子,Sobel算子,Laplace算子,Scharr滤波器合辑