Go语言中错误处理的方式总结

 更新时间:2023年07月05日 10:35:54   作者:242030  
这篇文章会结合 errors 中的函数,来讨论一下 Go 中常见的 error 使用方式,文中的示例代码讲解详细,具有一定的学习价值,需要的可以了解一下

Go 的 error 有两个很重要的特性:

  • error 就是一个普通的值,处理起来没有额外的开销
  • error 的扩展性很不错,可以按照不同的场景来自定义错误

在 Go1.13 之后,在 errors 包中提供了一些函数,让错误的处理和追踪更加方便一些。

这篇文章会结合 errors 中的函数,来讨论一下 Go 中常见的 error 使用方式。

这里说的 errors 包是指 Go 中的原生 errors 包。

1、原生 error

在 Go 的错误处理中,下面的代码占绝大多数:

if err != nil {
   return err
}

在满足业务需求的情况下,这种错误处理其实是最推荐的方式,这种直接透传的方式让代码之间的耦合度更低。在很多情况下,如果不关心错误中的具体信息,使用这种方式就可以了。

2、提前定义好 error

原生的 error 在有些情况下使用起来就不是很方便,比如我需要获得具体的错误信息,如果还用上面的方式来使用error,可能会出现下面的代码:

if err != nil && err.Error() == "invalid param" {
}

写过代码的都知道上面的代码很不优雅,另外如果错误的信息变化之后,这里的代码逻辑就会出错,可以通过把错误定义成一个变量:

var (
    ErrInvalidParam = errors.New("invalid param")
)

那么上面的代码就可以变成这样:

if err != nil && err == ErrInvalidParam {
}

如果一次性需要处理的错误比较多,还可以使用 switch 进行处理:

if err != nil {
    switch err {
    case ErrInvalidParam:
        return
    case ErrNetWork:
        return
    case ErrFileNotExist:
        return
    default:
        return
    }
}

但是这种方式还不完美,因为 error 在传递的过程中,有可能会被包装,以携带更多的堆栈信息,比如下面这样:

if err != nil {
    // 在包装错误的时候,这里格式化错误要使用%w
    return fmt.Errorf("add error info: %+v, origin error: %w", "other info", err)
}

假设上面被包装的错误是 ErrInvalidParam,那么在调用的地方判断错误,就不能使用下面的代码:

if err != nil && err == ErrInvalidParam {
}

为了解决这个问题, errors.Is 函数可以判断被包装的 error 中是否有预期的 error:

if errors.Is(err, ErrInvalidParam) {
}

尽量使用 errors.Is 来替代对 error 的比较。

3、使用自定义的错误类型

上面的 error 使用方式在某些情况下还是不能满足要求。假如对于上面的无效参数 error,业务方想要知道具体是哪个参数无效,直接定义的错误就无法满足要求。error 本质是一个接口,也就是是说,只要实现了 Error 方法,就是一个 error 类型:

type error interface {
    Error() string
}

那么就可以自定义一种错误类型:

type ErrInvalidParam struct {
    ParamName  string
    ParamValue string
}
func (e *ErrInvalidParam) Error() string {
    return fmt.Sprintf("invalid param: %+v, value: %+v", e.ParamName, e.ParamValue)
}

然后就可以使用类型断言机制或者类型选择机制,来对不同类型的错误进行处理:

e, ok := err.(*ErrInvalidParam)
if ok && e != nil {
}

同样可以在 switch 中使用:

if err != nil {
    switch err.(type) {
    case *ErrInvalidParam:
        return
    default:
        return
    }
}

在这里 error 同样会存在被包装的问题,而 errors.As 刚好可以用来解决这个问题,可以判断出被包装的错误中是否存在某个 error 类型:

var e *ErrInvalidParam
if errors.As(err, &e) {
}

4、更灵活的 error 类型

上面的方式已经可以解决大部分场景的 error 处理了,但是在一些复杂的情况下,可能需要从错误中获取更多的信息,还包含一定的逻辑处理。

在 Go 的 net 包中,有这样的一个接口:

type Error interface {
    error
    Timeout() bool  
    Temporary() bool
}

在这个接口中,有两个方法,这两个方法会对这个错误类型进行处理,判断是超时错误还是临时错误,实现了这个接口的 error 要实现这两个 方法,实现具体的判断逻辑。

在处理具体 error 时,会调用相应的方法来判断:

if ne, ok := e.(net.Error); ok && ne.Temporary() { 
     // 对临时错误进行处理 
}
if ne, ok := e.(net.Error); ok && ne.Timeout() { 
     // 对超时错误进行处理 
}

这种类型的 error 相对来说,使用的会比较少,一般情况下,尽量不要使用这么复杂的处理方式。

5、errors 中的其他能力

在 errors 包中,除了上面提到的 errors.Is 和 errors.As 两个很有用的函数之外,还有一个比较实用的函数errors.Unwrap,这个函数可以从包装的错误中将原错误解析出来。

可以使用 fmt.Errorf 来包装 error,需要使用 %w 的格式化:

return fmt.Errorf("add error info: %+v, origin error: %w", "other info", err)

在后续的 error 处理时,可以调用 errors.Unwrap 函数来获得被包装前的 error:

err = errors.Unwrap(err)
fmt.Printf("origin error: %+v\n", err)
package main
import (
	"errors"
	"fmt"
)
func main(){
	err1 := errors.New("zero")
	fmt.Printf("origin error: %+v\n", err1)
	err2 := fmt.Errorf("add error info: %+v, origin error: %w", "other info", err1)
	fmt.Printf("origin error: %+v\n", err2)
	err3 := errors.Unwrap(err2)
	fmt.Printf("origin error: %+v\n", err3)
}

程序输出

origin error: zero
origin error: add error info: other info, origin error: zero
origin error: zero

6、注意

如果需要使用 goroutine 时,应该使用统一的 Go 函数进行创建,这个函数中会进行 recover ,避免因为野生goroutine panic 导致主进程退出。

func Go(f func()){
    go func(){
        defer func(){
            if err := recover(); err != nil {
                log.Printf("panic: %+v", err)
            }
        }()
        f()
    }()
}

在应用程序中出现错误时,使用 errors.New 或者 errors.Errorf 返回错误:

errors.Errorf("用户余额不足, uid: %d, money: %d", uid, money)

如果是调用应用程序的其他函数出现错误,请直接返回,如果需要携带信息,请使用 errors.WithMessage。

// https://github.com/pkg/errors
errors.WithMessage(err, "其他附加信息")

如果是调用其他库(标准库、企业公共库、开源第三方库等)获取到错误时,请使用 errors.Wrap 添加堆栈信息。

切记,不要每个地方都是用 errors.Wrap 只需要在错误第一次出现时进行 errors.Wrap 即可。

根据场景进行判断是否需要将其他库的原始错误吞掉,例如可以把 repository 层的数据库相关错误吞掉,返回业务错误码,避免后续我们分割微服务或者更换 ORM 库时需要去修改上层代码。

注意我们在基础库,在大量引入的第三方库编写时一般不使用 errors.Wrap 避免堆栈信息重复。

func f() error {
    err := json.Unmashal(&a, data)
    if err != nil {
        return errors.Wrap(err, "其他附加信息")
    }
    // 其他逻辑
    return nil
}

禁止每个出错的地方都打日志,只需要在进程的最开始的地方使用 %+v 进行统一打印,例如 http/rpc 服务的中间件。

错误判断使用 errors.Is 进行比较:

func f() error {
    err := A()
    if errors.Is(err, io.EOF){
        return nil
    }
    // 其他逻辑
    return nil
}

错误类型判断,使用 errors.As 进行赋值:

func f() error {
    err := A()
    var errA errorA
    if errors.As(err, &errA){
    }
    // 其他逻辑
    return nil
}

以上就是Go语言中错误处理的方式总结的详细内容,更多关于Go错误处理的资料请关注脚本之家其它相关文章!

相关文章

  • Go语言的Windows下环境配置以及简单的程序结构讲解

    Go语言的Windows下环境配置以及简单的程序结构讲解

    这篇文章主要介绍了Go语言的Windows下环境配置以及简单的程序结构讲解,从编程语言约定俗成的hellow world开始,需要的朋友可以参考下
    2015-10-10
  • 入门学习Go的基本语法

    入门学习Go的基本语法

    这篇文章主要介绍了Go语法的基本概念,本文讲解了Golang的基本知识、基础语法、相关术语等,需要的朋友可以参考下,希望能帮助到你
    2021-07-07
  • Go语言生成素数的方法

    Go语言生成素数的方法

    这篇文章主要介绍了Go语言生成素数的方法,实例分析了Go语言生成素数的技巧,需要的朋友可以参考下
    2015-03-03
  • 创建第一个Go语言程序Hello,Go!

    创建第一个Go语言程序Hello,Go!

    这篇文章主要介绍了创建第一个Go语言程序Hello,Go!本文详细的给出项目创建、代码编写的过程,同时讲解了GOPATH、Go install等内容,需要的朋友可以参考下
    2014-10-10
  • golang(gin)的全局统一异常处理方式,并统一返回json

    golang(gin)的全局统一异常处理方式,并统一返回json

    这篇文章主要介绍了golang(gin)的全局统一异常处理方式,并统一返回json,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-01-01
  • go语言LeetCode题解720词典中最长的单词

    go语言LeetCode题解720词典中最长的单词

    这篇文章主要为大家介绍了go语言LeetCode题解720词典中最长的单词,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-12-12
  • 利用go-zero在Go中快速实现JWT认证的步骤详解

    利用go-zero在Go中快速实现JWT认证的步骤详解

    这篇文章主要介绍了如何利用go-zero在Go中快速实现JWT认证,本文分步骤通过实例图文相结合给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2020-10-10
  • Go使用database/sql操作数据库的教程指南

    Go使用database/sql操作数据库的教程指南

    Go 语言中,有一个名为database/sql的标准库,提供了统一的编程接口,使开发人员能够以一种通用的方式与各种关系型数据库进行交互,本文就来和大家讲讲它的具体操作吧
    2023-06-06
  • go语言base64用法实例

    go语言base64用法实例

    这篇文章主要介绍了go语言base64用法,实例分析了Go语言base64编码的实用技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-02-02
  • Go语言自定义包构建自己的编程工具库

    Go语言自定义包构建自己的编程工具库

    Go 语言的强大不仅体现在其内置功能上,还在于其支持自定义包,这为开发者提供了极大的灵活性和可扩展性,本文将深入介绍如何创建、使用和管理自定义包,探索 Go 语言包的奥秘,打造属于你的编程工具库
    2023-11-11

最新评论