关于golang:如何在-Go-中优雅的处理和返回错误1函数内部的错误处理

50次阅读

共计 3629 个字符,预计需要花费 10 分钟才能阅读完成。

在应用 Go 开发的后盾服务中,对于错误处理,始终以来都有多种不同的计划,本文探讨并提出一种从服务内到服务外的谬误传递、返回和回溯的残缺计划,还请读者们一起探讨。

问题提出

在后盾开发中,针对错误处理,有三个维度的问题须要解决:

  • 函数外部的错误处理: 这指的是一个函数在执行过程中遇到各种谬误时的错误处理。这是一个语言级的问题
  • 函数 / 模块的错误信息返回: 一个函数在操作谬误之后,要怎么将这个错误信息优雅地返回,不便调用方(也要优雅地)解决。这也是一个语言级的问题
  • 服务 / 零碎的错误信息返回: 微服务 / 零碎在解决失败时,如何返回一个敌对的错误信息,仍然是须要让调用方优雅地了解和解决。这是一个服务级的问题,实用于任何语言

针对这三个维度的问题,笔者筹备写三篇文章一一阐明。首先本文就是第一篇: 函数外部的错误处理

高级语言的错误处理机制

一个面向过程的函数,在不同的处理过程中须要 handle 不同的错误信息;一个面向对象的函数,针对一个操作所返回的不同类型的谬误,有可能须要进行不同的解决。此外,在遇到谬误时,也能够应用断言的形式,疾速停止函数流程,大大提高代码的可读性。

在许多高级语言中都提供了 try ... catch 的语法,函数外部能够通过这种计划,实现一个对立的错误处理逻辑。而即使是 C 这种“中级语言”,尽管没有 try catch,然而程序员也能够应用宏定义配合 goto LABEL 的形式,来实现某种程度上的谬误断言和解决。

Go 的谬误断言

在 Go 的状况就比拟难堪了。咱们先来看断言,咱们的目标是,仅应用一行代码就可能查看谬误并终止以后函数。因为没有 throw、没有宏,如果要实现一行断言,有两种办法。

办法一:单行 if + return

第一种是把 if 的错误判断写在一行内,比方:

    if err != nil {return err}

这种办法有值得商讨的点:

  • 尽管合乎 Go 的代码标准,然而在实操中,if 语句中的花括号不换行这一点还是十分有争议的,并且笔者在理论代码中也很少见到过
  • 代码不够直观,大抵浏览代码的时候,断言代码不显眼,而且在花括号中除了 return 之外也没法别的了,起因是 Go 的标准中强烈不倡议应用 ; 来分隔多条语句(if 条件判断除外)

因而,笔者强烈不倡议这么做。

办法二:panic + recover

第二种办法是借用 panic 函数,联合 recover 来实现,如以下代码所示:

func SomeProcess() (err error)
    defer func() {if e := recover(); e != nil {err = e.(error)
        }
    }()

    assert := func(cond bool, e error) {
        if !cond {panic(e)
        }
    }

    // ...

    err = DoSomething()
    assert(err == nil, fmt.Errorf("DoSomething() error: %w", err))

    // ...
}

这种办法好不好呢?咱们要分状况看

首先,panic 的设计原意,是在当程序或协程遇到严重错误,齐全无奈持续运行上来的时候,才会调用(_比方段谬误、共享资源竞争谬误_)。这相当于 Linux 中 FATAL 级别的谬误日志,用这种机制,仅仅用来进行一般的错误处理(ERROR 级别),杀鸡用牛刀了。

其次,panic 调用自身,相比于一般的业务逻辑的零碎开销是比拟大的。而错误处理这种事件,可能是常态化逻辑,频繁的 panic - recover 操作,也会大大降低零碎的吞吐。

然而话虽这么说,应用 panic 来断言的计划,尽管在业务逻辑中基本上不必,但在测试场景下则是十分常见的。测试嘛,用牛刀有何不可?略微大一点的零碎开销也没啥问题。对于 Go 来说,十分热门的单元测试框架 goconvey 就是应用 panic 机制来实现单元测试中的断言,用的人都说好。

论断倡议

综上,在 Go 中,对于业务代码,笔者是不倡议采纳断言的,遇到谬误的时候倡议还是老老实实采纳这种格局:

if err := DoSomething(); err != nil {// ...}

而在单测代码中,则齐全能够大大方方地采纳相似于 goconvey 之类基于 panic 机制的断言。

Go 的 try … catch

家喻户晓,Go(以后版本 1.17)是没有 try ... catch 的,而且从官网的态度而言,短时间内也没有明确的打算。然而程序员有这个需要呀。这里也催生出了集中解决方案

defer 函数

笔者采纳的办法,是将须要返回的 err 变量在函数外部全局化,而后联合 defer 对立解决:

func SomeProcess() (err error) { // <-- 留神,err 变量必须在这里有定义
    defer func() {
        if err == nil {return}

        // 这上面的逻辑,就当作 catch 作用了
        if errors.Is(err, somepkg.ErrRecordNotExist) {err = nil        // 这里是举一个例子,有可能捕捉到某些谬误,对于该函数而言不算谬误,因而 err = nil} else if errors.Like(err, somepkg.ErrConnectionClosed) {// ...            // 或者是说遇到连贯断开的操作时,可能须要做一些重连操作之类的;甚至乎还能够在这里重连胜利之后,从新拉起一次申请} else {// ...}
    }()

    // ...

    if err = DoSomething(); err != nil {return}

    // ...
}

这种计划要特地留神变量作用域问题。

比方后面的 if err = DoSomething(); err != nil { 行,如果咱们将 err = ... 改为 err := ...,那么这一行中的 err 变量和函数最后面定义的 (err error) 不是同一个变量,因而即使在此处产生了谬误,然而在 defer 函数中无奈捕捉到 err 变量了。

try ... catch 方面,笔者其实没有特地好的办法来模仿,即使是下面的办法也有一个很让人头疼的问题:defer 写法导致错误处理前置,而失常逻辑后置了。

命名的谬误处理函数

要解决前文提及的 defer 写法导致错误处理前置的问题,有第一种解决办法是比拟惯例的,那就是将 defer 前面的匿名函数改成一个命名函数,形象出一个专门的谬误处理函数。这个时候咱们能够将上一段函数进行这样的革新:

func SomeProcess() error {
    // ...

    if err = DoSomething(); err != nil {return unifiedError(err)
    }

    // ...
}

func unifiedError(err error) error {if errors.Is(err, somepkg.ErrRecordNotExist) {return nil        // 有可能捕捉到某些谬误,对于该函数而言不算谬误,因而 err = nil} else if errors.Like(err, somepkg.ErrConnectionClosed) {return fmt.Errorf("handle XXX error: %w", err)

    // ...

    } else {return err}
}

这样就难受一些了,至多逻辑前置,错误处理后置。不过读者必定会发现——这不是什么语言都能够这么搞嘛?诚然,这怎么看都不像是对 try ... catch 的模仿, 但这种办法仍然很举荐,特地是错误处理代码很长的时候。

goto LABEL

实践上,咱们能够通过 goto 语句,将错误处理后置,比方:

func SomeProcess() error {
    // ...

    if err = DoSomething(); err != nil {goto ERR}

    // ...

    return nil

ERR:
    // ...
}

C 语言比拟相熟的同学可能会感觉很亲切,因为在 Linux 内核中就有大量这种写法。这种写法呢,笔者其实说不出具体不好的中央,然而这个看起来很像 C 的写法,其实限度很多,反而比起 C 而言,须要留神的中央也更多:

  • 仅限于 ANSI-C 的话,要求所有的局部变量都须要前置申明,这就防止了因为变量作用域而带来的同名变量笼罩;但 Go 须要留神这个问题。
  • C 反对宏定义,配合前文能够实现断言,使得错误处理语句能够做得比拟优雅;而 Go 不反对
  • Go 常常有很多匿名函数,匿名函数无奈 goto 到外层函数的标签,这也限度了 goto 的应用

不过笔者倒也不是不反对应用 goto,只是感觉在现有机制下,还是应用前两种模式比拟合乎 Go 的习惯。


下一篇文章是《如何在 Go 中优雅的解决和返回谬误(2)——函数 / 模块的错误信息返回》,笔者具体整顿了 Go 1.13 之后的 error wrapping 性能,敬请期待~~


本文章采纳 常识共享署名 - 非商业性应用 - 雷同形式共享 4.0 国内许可协定 进行许可。

本文最早公布于云 + 社区,也是 amc 的博客。

原作者:amc,欢送转载,但请注明出处。

原文题目:《如何在 Go 中优雅的解决和返回谬误(1)——函数外部的错误处理》

公布日期:2021-09-30

原文链接:https://segmentfault.com/a/1190000040762538。

正文完
 0