Go 的错误处理

82 阅读7分钟

Go 语言的内置错误接口

Go 语言中的内置的错误接口提供了非常简单的错误处理机制。
error 类型 是一个内置的 接口类型,这是他在源码中的定义:

//error 接口内有一个返回字符串的方法Error()
type error interface {
    Error() string
}

error 是一个带有 Error() 方法的接口类型,这意味着你可以自己去实现这个接口
error 接口内只有一个方法Error(),,只要实现了这个方法就是实习拿了error。

实现Go 的内置错误接口

我们可以在编码中通过实现error 接口类型(即实现errro接口中的方法)来生成错误信息。

方法一:在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 的内置函数生成错误信息

errors.New()

error.New 在源码中的声明:

//参数是一个字符串,返回一个错误信息
func New(text string) error

解释:自己输入一个字符串该函数生成(创建)并返回一个error类型数据。

使用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
使用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
}

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.

Go的一些新增内置错误处理方法

新增方法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

使用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 进行了关联,就可以使用 error.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