Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > Golang标准库之errors包应用

Golang标准库之errors包应用方式

作者:一只coding猪

Go语言的errors包提供了基础的错误处理能力,允许通过errors.New创建自定义error对象,error在Go中是一个接口,通过实现Error方法来定义错误文本,对错误的比较通常基于对象地址,而非文本内容,因此即使两个错误文本相同

一. errors的基本应用

errors包是一个比较简单的包,包括常见的errors.New创建一个error对象,或通过error.Error方法获取error中的文本内容,本质上在builtin类型中,error被定义为一个interface,这个类型只包含一个Error方法,返回字符串形式的错误内容。

应用代码很简单:

// 示例代码
func Oops() error {
	return errors.New("iam an error")
}

func Print() {
	err := Oops()
	fmt.Println("oops, we go an error,", err.Error())
}

通过errors.New方法,可以创建一个error对象,在标准库实现中,对应了一个叫errorString的实体类型,是对error接口的最基本实现。

二. 错误类型的比较

代码中经常会出现err == nil 或者err == ErrNotExist之类的判断,对于error类型,由于其是interface类型,实际比较的是interface接口对象实体的地址。

也就是说,重复的new两个文本内容一样的error对象,这两个对象并不相等,因为比较的是这两个对象的地址。这是完全不同的两个对象

// 展示了error比较代码
if errors.New("hello error") == errors.New("hello error") { // false
}
errhello := errors.New("hello error")
if errhello == errhello { // true
}

在通常的场景中,能掌握errors.New()、error.Error()以及error对象的比较,就能应付大多数场景了,但是在大型系统中,内置的error类型很难满足需要,所以下面要讲的是对error的扩展。

三. error的扩展

3.1 自定义error

go允许函数具有多返回值,但通常你不会想写太多的返回值在函数定义上(looks ugly),而标准库内置的errorString类型由于只能表达字符串错误信息显然受限。所以,可以通过实现error接口的方式,来扩展错误返回

// 自定义error类型
type EasyError struct {
	Msg  string	// 错误文本信息
	Code int64	// 错误码
}

func (me *EasyError) Error() string {
	// 当然,你也可以自定义返回的string,比如
	// return fmt.Sprintf("code %d, msg %s", me.Code, me.Msg)
	return me.Msg
}

// Easy实现了error接口,所以可以在Oops中返回
func DoSomething() error {
	return &EasyError{"easy error", 1}
}

// 业务应用
func DoBusiness() {
	err := DoSomething()
	e,ok := err.(EasyError)
	if ok {
		fmt.Printf("code %d, msg %s\n", e.Code, e.Msg)
	}
}

现在在自定义的错误类型中塞入了错误码信息。随着业务代码调用层层深入,当最内层的操作(比如数据库操作)发生错误时,我们希望能在业务调用链上每一层都携带错误信息,就像递归调用一样,这时可以用到标准库的Unwrap方法

3.2 Unwrap与Nested error

一旦你的自定义error实现类型定义了Unwrap方法,那么它就具有了嵌套的能力,其函数原型定义如下:

// 标准库Unwrap方法,传入一个error对象,返回其内嵌的error
func Unwrap(err error) error

// 自定义Unwrap方法
func (me *EasyError) Unwrap() error {
	// ... 
}

虽然error接口没有定义Unwrap方法,但是标准库的Unwrap方法中会通过反射隐式调用自定义类型的Unwrap方法,这也是业务实现自定义嵌套的途径。我们给EasyError增加一个error成员,表示包含的下一级error

// 
type EasyError struct {
	Msg  string	// 错误文字信息
	Code int64	// 错误码
	Nest error 	// 嵌套的错误
}

func (me *EasyError) Unwrap() error {
	return me.Nest
}

func DoSomething1() error {
	// ...
	err := DoSomething2()
	if err != nil {
		return &EasyError{"from DoSomething1", 1, err}
	}

	return nil
}

func DoSomething2() error {
	// ...
	err := DoSomething3()
	if err != nil {
		return &EasyError{"from DoSomething2", 2, err}
	}

	return nil
}

func DoSomething3() error {
	// ...

	return &EasyError{"from DoSomething3", 3, nil}
}
// 可以很清楚的看到调用链上产生的错误信息
// Output:
// 	code 1, msg from DoSomething1
// 	code 2, msg from DoSomething2
// 	code 3, msg from DoSomething3
func main() {
	err := DoSomething1()
	for err != nil {
		e := err.(*EasyError)
		fmt.Printf("code %d, msg %s\n", e.Code, e.Msg)
		err = errors.Unwrap(err)		// errors.Unwrap中调用EasyError的Unwrap返回子error
	}
}

输出如下

$ ./sample
code 1, msg from DoSomething1
code 2, msg from DoSomething2
code 3, msg from DoSomething3

这样就可以在深入的调用链中,通过嵌套的方式,将调用路径中的错误信息,携带至调用栈的栈底。

对于不同模块,返回的错误信息大不相同,比如网络通信模块期望错误信息携带http状态码,而数据持久层期望返回sql或redis commend,随着模块化的职能划分,每个子模块可能会定义自己的自定义error类型,这时在业务上去区分不同类别的错误,就可以使用Is方法

3.3 errors.Is方法与错误分类

以网络错误和数据库错误为例,分别定义两种实现error接口的结构NetworkError和DatabaseError。

// 网络接口返回的错误类型
type NetworkError struct {
	Code   int	  // 10000 - 19999
	Msg    string // 文本信息
	Status int    // http状态码
}

// 数据库模块接口返回的错误类型
type DatabaseError struct {
	Code int	// 20000 - 29999
	Msg  string // 文本错误信息
	Sql  string // sql string
}

NetworkError与DatabaseError都实现了Error方法和Unwrap方法,代码里就不重复写了。错误类型的划分,导致上层业务对error的处理产生变化:业务层需要知道发生了什么,才能给用户提供恰当的提示,但是又不希望过分详细,比如用户期望看到的是“数据访问异常”、“请检查网络状态”,而不希望用户看到“unknown column space in field list…”、“request timeout…”之类的技术性错误信息。此时Is方法就派上用场了。

现在我们为网络或数据库错误都增加一个Code错误码,并且人为对错误码区间进行划分,[10000,20000)表示网络错误,[20000,30000)表示数据库错误,我们期望在业务层能够知道错误码中是否包含网络错误或数据访问错误,还需要为两种错误类型添加Is方法:

var(
	// 将10000和20000预留,用于在Is方法中判断错误码区间
	ErrNetwork  = &NetworkError{EasyError{"", 10000, nil}, 0}
	ErrDatabase = &DatabaseError{EasyError{"", 20000, nil}, ""}
)

func (ne NetworkError) Is(e error) bool {
	err, ok := e.(*NetworkError)
	if ok {
		start := err.Code / 10000
		return ne.Code >= 10000 && ne.Code < (start+1)*10000
	}
	return false
}

func (de DatabaseError) Is(e error) bool {
	err, ok := e.(*DatabaseError)
	if ok {
		start := err.Code / 10000
		return de.Code >= 10000 && de.Code < (start+1)*10000
	}
	return false
}

与Unwrap类似,Is方法也是被errors.Is方法隐式调用的,来看一下业务代码

func DoNetwork() error {
	// ...
	return &NetworkError{EasyError{"", 10001, nil}, 404}
}

func DoDatabase() error {
	// ...
	return &DatabaseError{EasyError{"", 20003, nil}, "select 1"}
}

func DoSomething() error {
	if err := DoNetwork(); err != nil {
		return err
	}
	if err := DoDatabase(); err != nil {
		return err
	}
	return nil
}

func DoBusiness() error {
	err := DoSomething()
	if err != nil {
		if errors.Is(err, ErrNetworks) {
			fmt.Println("网络异常")
		} else if errors.Is(err, ErrDatabases) {
			fmt.Println("数据访问异常")
		}
	} else {
		fmt.Println("everything is ok")
	}
	return nil
}

执行DoBusiness,输出如下:

$ ./sample
网络异常

通过Is方法,可以将一批错误信息归类,对应用隐藏相关信息,毕竟大部分时候,我们不希望用户直接看到出错的sql语句。

3.4 errors.As方法与错误信息读取

现在通过Is实现了分类,可以判断一个错误是否是某个类型,但是更进一步,如果我们想得到不同错误类型的详细信息呢?业务层拿到返回的error,就不得不通过层层Unwrap和类型断言来获取调用链中的深层错误信息。所以errors包提供了As方法,在Unwrap的基础上,直接获取error接口中,实际是error链中指定类型的错误。

所以在DatabaseError的基础上,再定义一个RedisError类型,作为封装redis访问异常的类型

// Redis模块接口返回的错误类型
type RedisError struct {
	EasyError
	Command string // redis commend
	Address string // redis instance address
}

func (re *RedisError) Error() string {
	return re.Msg
}

在业务层,尝试读取数据库和redis错误的详细信息

func DoDatabase() error {
	// ...
	return &DatabaseError{EasyError{"", 20003, nil}, "select 1"}
}

func DoRedis() error {
	// ...
	return &RedisError{EasyError{"", 30010, nil}, "set hello 1", "127.0.0.1:6379"}
}

func DoDataWork() error {
	if err := DoRedis(); err != nil {
		return err
	}
	if err := DoDatabase(); err != nil {
		return err
	}
	return nil
}

// 执行业务代码
func DoBusiness() {
	err := DoDataWork()
	if err != nil {
		if rediserr := (*RedisError)(nil); errors.As(err, &rediserr) {
			fmt.Printf("Redis exception, commend : %s, instance : %s\n", rediserr.Command, rediserr.Address)
		} else if mysqlerr := (*DatabaseError)(nil); errors.As(err, &mysqlerr) {
			fmt.Printf("Mysql exception, sql : %s\n", mysqlerr.Sql)
		}
	} else {
		fmt.Println("everything is ok")
	}
}

运行DoBusiness,输出如下

$ ./sample
Redis exception, commend : set hello 1, instance : 127.0.0.1:6379

conclusion

通过这些手段,可以在不侵入业务接口的情况下,丰富错误处理,这就是errors包带来的便利。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文