脚本之家,脚本语言编程技术及教程分享平台!
分类导航

Python|VBS|Ruby|Lua|perl|VBA|Golang|PowerShell|Erlang|autoit|Dos|bat|shell|

服务器之家 - 脚本之家 - Golang - Go版本大于1.13,程序里这样做错误处理才地道

Go版本大于1.13,程序里这样做错误处理才地道

2022-12-12 20:40网管叨bi叨卡尔文_ Golang

这篇文章主要是更新一下Error处理在Go 1.13以后新增的功能点,以前的文章介绍的更多的还是使用"pkg/errors"那个包的方式,主要是前两年以前公司用的Go版本一直是1.12,所以这部分知识我一直没更新过来,这里简单做个梳理。

大家好,这里是每周都在陪你进步的网管。

之前写过几篇关于 Go 错误处理的文章,发现文章里不少知识点都有点落伍了,比如Go在1.13后对错误处理增加了一些支持,最大的变化就是支持了错误包装(Error Wrapping),以前想要在调用链路的函数里包装错误都是用"github.com/pkg/errors"这个库。

Go 在2019年发布的Go1.13版本也采纳了错误包装,并且还提供了几个很有用的工具函数让我们能更好地使用包装错误。这篇文章就来主要说一下这方面的知识点,不过开始我们还是再次强调一下使用 Go Error 的误区,避免我们从其他语言切换过来时给自己后面挖坑。

自定义错误要实现error接口

这一条估计很多人都知道,但是文章开头开始先从这个惯例开始,因为我以前待过一个PHP转Go的研发团队,可能大家一开始都不太会,才有了这种错误的使用方式。

首先我们再复述一遍,Go通过error类型的值表示程序里的错误。

error类型是一个内建接口类型,该接口只规定了一个返回字符串值的Error方法。

type error interface { Error() string }

Go程序的函数经常会返回一个error值

package strconv

func Atoi(s string) (int, error) { .... }

调用者通过测试error值是否是nil来进行错误处理。

i, err := strconv.Atoi("42") if err != nil { fmt.Printf("couldn't convert number: %v\n", err) return } fmt.Println("Converted integer:", i)

error为nil时表示成功;非nil的error表示失败。

说完 Go 里 error 最基本的使用方式后,接下来说项目里的自定义错误类型。假如项目在 Dao 层定义了一个这样的错误类型来记录数据库查询错误。

type MyError struct { Sql   string
    Param string
    Err   error }

假如,这个自定义的MyError不去实现error接口,Dao 层里的函数返回的都是MyError的话。

func FindUserRowByPhoneMyError(userId int) (user User, MyError error) { ...... }

那么使用这些 Dao 函数的代码逻辑层都得引入dao.MyError这个额外的类型。有人会说,我把MyError定义在公共包里,所有代码逻辑层、Dao 层都用这个common.MyError总没啥问题了吧。

使用上乍一看没什么问题,但其实最大的问题就是不兼容、不符合Go语言对错误的接口约束,就没法对自定义错误类型使用Go对error提供的其他功能了,比如说后面要介绍的错误包装。

所以针对自定义的错误类型,我们也要让他变成一个真正的Go error,方法就是让它实现error接口定义的方法。

func (e *MyError) Error() string { return fmt.Sprintf("sql: %s, params: %s, err: %s", e.Sql, e.Param, e.Err.Error()) }

包装错误

在现实的程序应用里,一个逻辑往往要经多多层函数的调用才能完成,那在程序里我们的建议Error Handling 尽量留给上层的调用函数做,中间和底层的函数通过错误包装把自己要记的错误信息附加再原始错误上再返回给外层函数。

比如像下面这样:

func doAnotherThing() error { return errors.New("error doing another thing") } func doSomething() error { err := doAnotherThing() return fmt.Errorf("error doing something: %v", err) } func main() { err := doSomething() fmt.Println(err) }

这段代码从打印错误信息的输出上看没什么问题,但是深层次的问题很明显,我们丢失了原来的err,因为它已经被我们的fmt.Errorf函数转成一个新的字符串了。

基于这个背景,很多开源三方库提供了错误包装、追加错误调用栈等功能,用的最多的就是"github.com/pkg/errors"这个库,提供了下面几个主要的包装错误的功能。

//只附加新的信息
func WithMessage(err error, message string) error

//只附加调用堆栈信息
func WithStack(err error) error

//同时附加堆栈和信息
func Wrap(err error, message string) error

Go官方在2019年发布1.13版本,自己也增加了对错误包装的支持,不过并没有提供什么Wrap函数,而是扩展了fmt.Errorf函数,加了一个%w来生成一个包装错误。

e := errors.New("原始错误")
w := fmt.Errorf("外面包了一个错误%w", e)

Go1.13引入了包装错误后,同时为内置的errors包添加了3个函数,分别是Unwrap、Is和As。

先来聊聊Unwrap,顾名思义,它的功能就是为了获取到包装错误里那个被嵌套的error。

func Unwrap(err error) error {
    //先判断是否是wrapping error
 u, ok := err.(interface {
  Unwrap() error
 })
 //如果不是,返回nil
 if !ok {
  return nil
 }
 //否则则调用该error的Unwrap方法返回被嵌套的error
 return u.Unwrap()
}

这里需要注意的是,嵌套可以有很多层,我们调用一次errors.Unwrap函数只能返回往里一层的error,如果想获取更里面的,需要调用多次errors.Unwrap函数。最终如果一个error不是warpping error,那么返回的是nil。

如果想得到最原始的error,建议自己封装个工具函数,类似这样

func Cause(err error) error { for err != nil { err = errors.Unwrap(err) } return err }

对于我们文章开头定义的那个自定义错误MyError想要把它变成可包装的Error的话,还需要实现一个Unwrap()方法。

func (e *MyError) Unwrap() error { return e.Err }

有了包装错误后,像具体某种错误的判断和错误的类型转换也得需要跟进改一下才行。这就是errors包在1.13后新增的另外两个工具函数Is和As的作用。接下来我们一个个来说。

errors.Is

在Go 1.13之前没有包装错误的时候,程序里要判断是不是同一个error可以直接简单粗暴的:

if err == os.ErrNotExists { ...... }

这样我们就可以通过判断来做一些事情。但是现在有了包装错误后这样办法就不完美的,因为你根本不知道返回的这个err是不是一个嵌套的error,嵌套了几层。所以基于这种情况,Go为我们提供了errors.Is函数。

func Is(err, target error) bool

如果err和目标错误target是同一个,那么返回true。

如果err 是一个包装错误,目标错误target也包含在这个嵌套错误链中的话,那么也返回true。

下面是一个使用errors.Is判断是否是同一错误的例子。

var ErrDivideByZero = errors.New("divide by zero") func Divide(a, b int) (int, error) { if b == 0 { return 0, ErrDivideByZero } return a / b, nil } func main() { a, b := 10, 0 result, err := Divide(a, b) if err != nil { switch { case errors.Is(err, ErrDivideByZero): fmt.Println("divide zero error") default: fmt.Printf("unexpected division error: %+v\n", err) } return } fmt.Printf("%d / %d = %d\n", a, b, result) }

errors.As

同样在没有包装错误前,我们要把error 转换为一个具体类型的error,一般都是使用类型断言或者 type switch,其实也就是类型断言。

if pathErr, ok := err.(*os.PathError); ok { fmt.Println(pathErr.Path) }

但是有了包装错误之后,返回的err可能是已经被嵌套了,这种方式就不能用了,所以Go为我们在errors包里提供了As函数。

func As(err error, target interface{}) bool

As 函数所做的就是遍历错误的嵌套链,从里面找到类型符合的error,然后把这个error赋给target参数,这样我们在程序里就可以使用转换后的target了,因为这里有赋值,所以target必须是一个指针,这个也算是Go内置包里的一个惯例了,像json.Unmarshal也是这样。

所以把上面的例子用As 函数实现就变成了酱婶:

var pathErr *os.PathError if errors.As(err, pathErr) { fmt.Println(pathErr.Path) }

总结

这篇文章主要是更新一下Error处理在Go 1.13以后新增的功能点,以前的文章介绍的更多的还是使用"pkg/errors"那个包的方式,主要是前两年以前公司用的Go版本一直是1.12,所以这部分知识我一直没更新过来,这里简单做个梳理。

原文地址:https://mp.weixin.qq.com/s/SFbSAGwQgQBVWpySYF-rkw

延伸 · 阅读

精彩推荐
  • Golanggolang 中strings包的Replace的使用说明

    golang 中strings包的Replace的使用说明

    这篇文章主要介绍了golang 中strings包的Replace的使用说明,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    kph_Hajash11482021-04-20
  • Golanggo语言 nil使用避坑指南

    go语言 nil使用避坑指南

    这篇文章主要为大家介绍了go语言 nil使用避坑指南详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪...

    starine5782022-09-02
  • Golanggolang API开发过程的中的自动重启方式(基于gin框架)

    golang API开发过程的中的自动重启方式(基于gin框架)

    这篇文章主要介绍了golang API开发过程的中的自动重启方式(基于gin框架),本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的...

    千里之行,始于足下12312021-02-25
  • GolangGo语言增强版操作MySQL(SQLX)

    Go语言增强版操作MySQL(SQLX)

    上次咱们学习了如何使用Go操作Mysql,并且实现了简单的增删改查。但是相对来说,还有有点复杂的,可能那些大佬也都觉得繁琐叭。就又开发出了增强版查...

    Go语言进阶学习14332021-01-28
  • GolangGo语言hello world实例

    Go语言hello world实例

    这篇文章主要介绍了Go语言hello world实例,本文先是给出了hello world的代码实例,然后对一些知识点和技巧做了解释,需要的朋友可以参考下 ...

    junjie3182020-04-08
  • GolangGolang实现四种负载均衡的算法(随机,轮询等)

    Golang实现四种负载均衡的算法(随机,轮询等)

    本文介绍了示例介绍了Golang 负载均衡的四种实现,主要包括了随机,轮询,加权轮询负载,一致性hash,感兴趣的小伙伴们可以参考一下...

    Gundy_8612021-08-09
  • GolangGo语言实现顺序存储的线性表实例

    Go语言实现顺序存储的线性表实例

    这篇文章主要介绍了Go语言实现顺序存储的线性表的方法,实例分析了Go语言实现线性表的定义、插入、删除元素等的使用技巧,具有一定参考借鉴价值,需要的...

    OSC首席键客2932020-04-22
  • Golang详解如何利用GORM实现MySQL事务

    详解如何利用GORM实现MySQL事务

    为了确保数据一致性,在项目中会经常用到事务处理,对于MySQL事务相信大家应该都不陌生。这篇文章主要总结一下在Go语言中Gorm是如何实现事务的;感兴...

    1个俗人6542022-09-08