【Go】Go 错误处理

导读:本篇文章讲解 【Go】Go 错误处理,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com


一、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()

前言:

  1. 从上例中,我们可以发现 errors.New() 函数接收的参数是一个包含错误信息的 字符串 ,然后根据这个字符串返回一个错误类型值。然而这个字符串是写好的,不能被我们格式化地去控制。但是如果我们想更灵活地、格式化地去控制这个错误信息字符串,然后再返回错误类型,该怎么做呢?
  2. 格式化输出:我们已经知道,Go语言 通过调用 fmt.Sprintf() 函数,并给定占位符 %s 就可以格式化控制地打印出某个值的字符串表示形式。那我们是不是可以先用 fmt.Sprintf() 函数实现格式化控制,然后再调用 errors.New() 函数来返回错误值,来达到我们的格式化控制错误信息的目的。答案是肯定的。
  3. 实际上,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

参考链接

  1. Go 错误处理
  2. Go Errors 错误处理
  3. Go语言(golang)的错误(error)处理的推荐方案
  4. Go 学习笔记(64)— Go error.New 创建接口错误对象、fmt.Errorf 创建接口错误对象

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/119031.html

(0)
seven_的头像seven_bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!