一、Go 语言的内置错误接口
Go 语言通过 内置 的错误接口提供了非常简单的错误处理机制。
error 类型 是一个内置的 接口类型 ,这是它在源码中的定义:
//error 接口内有一个返回字符串的方法Error()
type error interface {
Error() string
}
源码所在: errors 包的源码放在 $GOROOT/src/errors 中
查看安装目录:go env GOROOT
error 是一个带有 Error() 方法的接口类型,这意味着你可以自己去实现这个接口。
error 接口内只有一个方法 Error() ,只要实现了这个方法,就是实现了error。
二、实现 Go 的内置错误接口
我们可以在编码中通过 实现 error 接口类型 (即实现 error 接口中的方法)来生成错误信息。
以下展示了三个错误生成方法。从基础到高级。推荐第三种方法。
方法一:在Error()方法中返回错误信息
自定义了一个fileError类型,实现了error接口:
package main
import (
"fmt"
)
//结构体 fileError
type fileError struct {
}
//在结构体 fileError 上实现 Error() 方法,相当于实现了 error 接口
func (fe *fileError) Error() string {
return "文件错误"
}
//经过以上两步已经实现了error这一接口数据类型!
func main() {
conent, err := openFile()
if err != nil {
fmt.Println(err)
} else {
fmt.Println(string(conent))
}
}
//只是模拟一个错误
func openFile() ([]byte, error) { //返回一个error类型的值
return nil, &fileError{}
}
输出结果:
文件错误
像以上这样编码存在一个问题:
在实际的使用过程中,我们可能遇到很多错误,他们错误信息并不一样,不都是“文件错误”。
一种做法是每种错误都类似上面一样定义一个错误类型,然后在实现 Error() 方法时返回错误信息,但是这样太麻烦了。我们发现 Error() 返回的其实是个字符串,我们可以修改下,使得这个字符串可以让我们自己设置就可以了。
方法二:通过传参返回错误信息
type fileError struct {
s string
}
func (fe *fileError) Error() string {
return fe.s
}
func openFile() ([]byte, error) {
return nil, &fileError{"文件错误,自定义"}
}
这样的话只需要在调用 fileError 接口时,更改字符串就可以了。
方法三:通过创建新的辅助函数返回错误信息
恩,可以了,已经达到了我们的目的。现在我们可以把它变的更通用一些,比如修改fileError的名字,再 创建一个辅助函数 ,便于我们创建不同的错误类型。
func New(text string) error {
return &errorString{text}
}
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
变成以上这样,我们就可以通过调用 New 函数,辅助我们创建不同的错误了。这其实就是我们经常用到的errors.New()
函数,被我们一步步剖析演化而来,现在大家对Go语言内置的错误error有了一个清晰的认知了。
说到这里,我们不得不先简单介绍一下 errors.New()
:
errors.New():”errors”包中的一个内置方法New()。使用的时候需要 import “errors”。
这是一种最基本的 生成错误值 的方式。
调用它的时候传入一个由 字符串 代表的错误信息,它会给返回给我们一个包含了这个错误信息的 error 类型值 。
三、使用 Go 的内置函数生成错误信息
1. errors.New()
在上一节末尾提到,errors.New()是 “errors” 包中的一个内置方法。
我们导入”errors”包之后,就可以使用errors.New()来生成错误信息了。
我们先来看看errors.New在源码中的声明:
//参数是一个字符串,返回一个错误信息
func New(text string) error
解释:自己输入一个字符串参数,该函数生成(创建)并返回一个 error 类型数据。
接下来我将通过两个程序实例展示errors.New()函数的用法。
(1)使用 errors.New() 的实例1:
package main
import "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被抛出之后,如果程序里没有任何保护措施的话,程序就会打印出panic的详情,然后终止运行。
}
}
输出结果:
panic: division by zero
goroutine 1 [running]:
main.main()
D:/liteide/mysource/src/hello/main.go:18 +0x77
(2)使用 errors.New() 的实例2:
函数通常在最后的返回值中返回错误信息。例如计算开方的函数:
func Sqrt(f float64) (float64, error) {
if f < 0 {
//errors.New方法生成一个错误信息
return 0, errors.New("math: square root of negative number")
}
// 实现
}
在调用这个函数时,如何判断有没有出错呢?
我们通过给Sqrt函数设置返回值来判断。
我们调用Sqrt的时候传递一个负数,由于不能对负数开方,所以出错,然后就返回了非空的error对象(说明有错),通过判断err是否非空(是否为 nil)来打印错误信息来报错。所以fmt.Println(fmt包在处理error时会调用Error方法)被调用,以输出错误。请看下面调用的示例代码:
result, err:= Sqrt(-1)
if err != nil {
fmt.Println(err)
}
判断是否为某个特定的错误:
var ErrNotFound = errors.New("not found") //将errors.New返回的错误类型
if err == ErrNotFound {
// something wasn't found
}
2. fmt.Errorf()
前言:
- 从上例中,我们可以发现 errors.New() 函数接收的参数是一个包含错误信息的 字符串 ,然后根据这个字符串返回一个错误类型值。然而这个字符串是写好的,不能被我们格式化地去控制。但是如果我们想更灵活地、格式化地去控制这个错误信息字符串,然后再返回错误类型,该怎么做呢?
- 格式化输出:我们已经知道,Go语言 通过调用 fmt.Sprintf() 函数,并给定占位符 %s 就可以格式化控制地打印出某个值的字符串表示形式。那我们是不是可以先用 fmt.Sprintf() 函数实现格式化控制,然后再调用 errors.New() 函数来返回错误值,来达到我们的格式化控制错误信息的目的。答案是肯定的。
- 实际上,2中的两步工作,一步就可以完成。秘诀是调用 fmt.Errorf() 函数。也就是说,当我们想通过模板化的、格式化的方式生成错误信息,并得到错误值时,可以使用 fmt.Errorf 函数。该函数所做的其实就是先调用 fmt.Sprintf 函数,得到确切的错误信息;再调用 errors.New 函数,得到包含该错误信息的 error 类型值,最后返回该值。
多一嘴:
对于 error 类型值,它的字符串表示形式则取决于它的 Error 方法。
也就是说,fmt.Printf 函数如果发现被打印的值是一个 error 类型的值,那么就会去调用它的 Error 方法。fmt 包中的这类打印函数其实都是这么做的。
验证实例:
package main
import (
"errors"
"fmt"
)
func main() {
err1 := fmt.Errorf("invalid contents: %s", "error")
err2 := errors.New(fmt.Sprintf("invalid contents: %s", "error"))
if err1.Error() == err2.Error() {
fmt.Println("The error messages in err1 and err2 are the same.")
} else {
fmt.Println("The error messages in err1 and err2 are different.")
}
}
输出结果:
The error messages in err1 and err2 are the same.
这说明 err1 和 err2 等价,即 fmt.Errorf("%s",string)
函数的功能就是 fmt.Sprintf("%s",string) + errors.New()
。
将 err1 和 err2 输出的字符串改一下:
package main
import (
"errors"
"fmt"
)
func main() {
err1 := fmt.Errorf("invalid contents: %s", "error1")
err2 := errors.New(fmt.Sprintf("invalid contents: %s", "error2"))
fmt.Println(err1)
fmt.Println(err2)
if err1.Error() == err2.Error() {
fmt.Println("The error messages in err1 and err2 are the same.")
} else {
fmt.Println("The error messages in err1 and err2 are different.")
}
}
输出结果:
invalid contents: error1
invalid contents: error2
The error messages in err1 and err2 are different.
四、Go 的一些新增内置错误处理方法
1.13 版本之前的错误处理
1.13 版本之前只有我上面讲的 errors.New() 和 fmt.Errorf() 两种方法。
处理错误的时候我们通常会使用这些方法添加一些额外的信息,记录错误的上下文以便于后续排查:
if err != nil {
return fmt.Errorf("错误上下文 %v: %v", name, err)
}
但是,fmt.Errorf 方法(上述这两种方法)会 创建 一个包含有原始错误文本信息的新的 error ,但是与原始错误之间是 没有任何关联 的。
然而我们有时候是需要 保留这种关联性 的,这时候就需要我们自己去定义一个包含有原始错误的新的错误类型 ,比如自定义一个 QueryError :
type QueryError struct {
Query string
Err error // 与原始错误关联
}
然后可以判断这个原始错误是否为某个特定的错误,比如 ErrPermission :
if e, ok := err.(*QueryError); ok && e.Err == ErrPermission {
// query failed because of a permission problem
}
写到这里,你可以发现对于错误的关联嵌套情况处理起来是比较麻烦的,而 Go 1.13 版本对此做了改进。
1.13 版本之后的错误处理
首先需要说明的是,Go 是向后兼容的,上文中的 1.13 版本之前的用法完全可以继续使用。
1.13 版本的改进是:
新增方法1: errors.Unwrap
func Unwrap(err error) error
对于错误嵌套的情况,Unwrap 方法可以用来 返回某个错误所包含的底层错误 ,例如 e1 包含了 e2 ,这里 Unwrap e1 就可以得到 e2 。Unwrap 支持链式调用(处理错误的多层嵌套)。
新增方法2: errors.Is
func Is(err, target error) bool
新增方法3: errors.As
func As(err error, target interface{}) bool
使用 errors.Is 和 errors.As 方法检查错误:
errors.Is 方法检查值:
if errors.Is(err, ErrNotFound) {
// something wasn't found
}
errors.As 方法检查特定错误类型:
var e *QueryError
if errors.As(err, &e) {
// err is a *QueryError, and e is set to the error's value
}
errors.Is 方法会对嵌套的情况展开判断,这意味着:
if e, ok := err.(*QueryError); ok && e.Err == ErrPermission {
// query failed because of a permission problem
}
可以直接简写为:
if errors.Is(err, ErrPermission) {
// err, or some error that it wraps, is a permission problem
}
新增4:fmt.Errorf 新增了 %w 格式化动词
fmt.Errorf 方法新增了 %w 格式化动词,其返回的 error 自动实现了 Unwrap 方法。
fmt.Errorf 方法通过 %w 包装错误:
if err != nil {
return fmt.Errorf("错误上下文 %v: %v", name, err)
}
上面的代码通过 %v 直接返回一个与原始错误无法关联的新错误。
我们使用 %w 就可以进行关联了:
if err != nil {
// Return an error which unwraps to err.
return fmt.Errorf("错误上下文 %v: %w", name, err)
}
一旦使用 %w 进行了关联,就可以使用 errors.Is 和 errors.As 方法了:
err := fmt.Errorf("access denied: %w”, ErrPermission)
...
if errors.Is(err, ErrPermission) ...
对于是否包装错误以及如何包装错误并没有统一的答案。
五、一个实例
在这里展示整数除法的错误处理:
我们不使用 errors 包,自己实现 Error() 方法,并使用 Sprintf() 格式化返回错误信息。
package main
import (
"fmt"
)
//被除数dividee 除以 除数divider 等于 商
//除数divider不能为0
// 定义一个 DivideError 结构体
//结构体中有被除数和除数
type DivideError struct {
dividee int
divider int
}
// 实现 `error` 接口 的方法Error()
func (de *DivideError) Error() string {
strFormat := `
Cannot proceed, the divider is zero.
dividee: %d
divider: 0
`
return fmt.Sprintf(strFormat, de.dividee)
}
// 定义 `int` 类型除法运算的函数
func Divide(varDividee int, varDivider int) (result int, errorMsg string) {
if varDivider == 0 { //除数等于0时报错
dData := DivideError{
dividee: varDividee,
divider: varDivider,
}
errorMsg = dData.Error()
return
} else {
return varDividee / varDivider, ""
}
}
func main() {
// 正常情况
if result, errorMsg := Divide(100, 10); errorMsg == "" {
fmt.Println("100/10 = ", result)
}
// 当除数为零的时候会返回错误信息
if _, errorMsg := Divide(100, 0); errorMsg != "" {
fmt.Println("errorMsg is: ", errorMsg)
}
}
输出结果:
100/10 = 10
errorMsg is:
Cannot proceed, the divider is zero.
dividee: 100
divider: 0
参考链接
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/119031.html