Go 错误处理指北

直接跳转查看错误处理最佳实践

参考文章

Go 错误处理指北:Error vs Exception vs ErrNo

Go 错误处理指北:pkg/errors 源码解读

Go 错误处理指北:如何优雅的处理错误?

回顾错误处理

Exception 派系

Exception 派系的错误处理机制有Java、Python、C#、JavaScript等主流的高级语言,他们错误处理的特点在于抛出异常、处理异常、异常兜底。

异常兜底的处理方式简化了代码编写,我们不需要在每一处函数调用处都对异常进行处理,下层函数在遇到错误时直接向上抛出即可。同样的,我们处理错误也只需要在最上层做一次异常处理即可。这里举一个Python代码的例子。

def a():
    ...

def b():
    ...

def c():
    ...

def main():
    try:
        a()
        b()
        c()
    except Exception as e:
        logging.error(e)
    else:
        print("success")
    finally:
        print("release resources")

这种方式极大的简化的错误编写,但是因为有异常兜底,开发者在写代码时就会更加随心所欲,程序会更加的不可控制。开发者会倾向于不将错误思考的那么细致,不再仔细思考什么情况会遇到什么错误。如果开发者滥用异常兜底(如 catch Exceptionexcept Exception,把所有异常一网打尽,然后继续执行),原本用于表示严重问题的异常就被当成普通流程分支,导致真正的错误被悄悄吞掉。这样会隐藏失败、丢失错误的严重级别、让日志缺失、使程序在错误状态下继续运行,并让业务逻辑变得不可预测。

C错误处理

由于C语言只能返回一个返回值,所以说通常情况下,有三种方法进行错误处理:

  1. 返回值既表示结果也表示错误,比如 fopen 函数。我们用该函数来打开一个文件,其返回值 file 可能是 FILE* 表示文件描述符,也可能是 NULL 表示函数出错。所以我们需要通过 if (file == NULL) 来判断打开文件是否出错,如果出错,全局变量 errno 会被赋值,使用 perror 函数或者 strerror(errno) 可以读取错误码对应的错误描述信息。这样的返回值就具有二义性,会存在一些问题,比如说让人忘记错误检查。
  2. 返回值为错误,结果作为参数,用参数列表传递结果指针。例如int div(int a, int b, int *result)div 函数还是单返回值,不过这个返回值只代表错误,返回 0 表示成功,返回 -1 表示失败。div 函数计算结果通过参数的形式,被存储在指针所指向的变量 int *result 中。但这种用参数接收计算结果的方式,语义不够清晰
  3. C 函数支持返回结构体指针,将错误和结果统一放在结构体。这种方式代码实现比较复杂,不过多赘述。

Go错误处理

得益于 Go 函数支持多返回值,所以 Go 就不再需要 errno 了。Go 定义了一个 error 类型,专门用来表示错误,使用 errors.New 可以轻松构建一个错误对象。

因此我们就有了 Go 中经典的错误处理:

if err != nil {
    return nil, err
}

我们将在之后的篇幅中讲Go错误处理的最佳实现。

一些错误处理思想

暂存错误状态(AND)

在使用 Builder 模式、链式调用或者 for 循环等场景下,暂存中间过程所出现的错误,有助于简化代码,使编写出的代码逻辑更加连贯。

例如以下代码:

// Get takes name of the pod, and returns the corresponding pod object, and an error if there is any.
func (c *pods) Get(ctx context.Context, name string, options metav1.GetOptions) (result *v1.Pod, err error) {
    result = &v1.Pod{}
    err = c.client.Get().
    Namespace(c.ns).
    Resource("pods").
    Name(name).
    VersionedParams(&options, scheme.ParameterCodec).
    Do(ctx).
    Into(result)
    return
}

// Namespace applies the namespace scope to a request (<resource>/[ns/<namespace>/]<name>)
func (r *Request) Namespace(namespace string) *Request {
    if r.err != nil {
        return r
    }
    if r.namespaceSet {
        r.err = fmt.Errorf("namespace already set to %q, cannot change to %q", r.namespace, namespace)
        return r
    }
    if msgs := IsValidPathSegmentName(namespace); len(msgs) != 0 {
        r.err = fmt.Errorf("invalid namespace %q: %v", namespace, msgs)
        return r
    }
    r.namespaceSet = true
    r.namespace = namespace
    return r
}

其中的*Request.Namespace 方法首先会通过 if r.err != nil 判断是否存在错误,如果存在则直接返回,不再继续执行。如果不存在错误,则接下来每次可能出现错误的调用,都会将错误信息暂存到 r.err 属性中。每个链上的函数都是这种执行逻辑,简化了调用过程出现错误的错误处理。

这种方式类似于AND逻辑,其中有一个err就中止执行,记录错误最后返回。

Try Sequence(OR)

刚刚我们提到的暂存错误状态适用于AND的逻辑,即一系列函数,其中有一个err就中止执行,记录错误最后返回。

我们也可能会遇到OR的逻辑,比如说我希望一系列函数,只要有一个成功就不报错,只有所有失败才报错,那这时我们会用Try Sequence来简化代码逻辑,如下所示。

func TryOr(funcs ...func() (string, error)) (string, error) {
    var errs []error

    for _, f := range funcs {
        v, err := f()
        if err == nil { // 有一个成功就直接返回
            return v, nil
        }
        errs = append(errs, err)
    }

    // 都失败:把错误合并返回
    //(取决于你自己,如果你只想返回最后一个错误,不用err list,只用err记录最后一个即可)
    return "", fmt.Errorf("all failed: %v", errs)
}

错误只应处理一次

虽然,不应该忽略你的错误,但错误也不应该被重复处理,你应该只处理一次错误。

处理错误意味着检查错误值并做出决定。也就是说,根据能否处理错误,我们实际上只有两种选择:

  1. 不能处理:直接向上传递错误,自身不对错误做任何假设,也就是 Opaque error
  2. 可以处理:降级处理,并向上返回 nil,因为自己已经处理了错误,表示不再有错误了,上层不应该继续拿到这个错误。

记录日志也属于处理错误,如果每次错误传递时都记录日志,那么最终看到的日志将会有非常多的重复内容。

何时记录错误日志

记录日志其实是一个比较大的话题,并且存在一定争议。何时记录、记录什么以及如何更好的记录都是比较复杂的问题。更糟糕的是,对于不同项目,可能会有不同的答案。

所以,这里只提一种可能适用的记录日志准则。

其实核心还是一句话:错误只应处理一次。

  1. 只在调用函数的最上层记录一次错误日志,调用链中间遇到错误直接返回,不做任何处理(或附加当前调用处的错误信息后返回)。
  2. 如果遇到服务降级的情况(为了防止主要功能受影响,我们会临时关闭非核心功能或使用简化的替代方案),我们也可以记录日志,并返回 nil,不再继续向上报告当前错误。
  3. 最好将错误堆栈也记录到错误中,在记录错误日志时用 %+v 打印。

不要做冗余的错误检查

不要写出这种代码:

func Foo() error {
    err := Bar()
    if err != nil {
        return err
    }
    return nil
}

正确写法如下:

func Foo() error {
    return Bar()
}

我们无需编写这种代码:

func Bar() error {
    err := Foo()
    if err != nil {
        return errors.WithMessage(err, "bar")
    }
    return nil
}

可以直接去掉那冗余的错误检查:

func Bar() error {
    err := Foo()
    return errors.WithMessage(err, "bar")
}

这不对执行结果造成任何影响。

我们无需判断 err 是否为 nil,因为 pkg/errors 内部的方法帮我们做好了这项检查:

func WithMessage(err error, message string) error {
    if err == nil {
        return nil
    }
    return &withMessage{
        cause: err,
        msg:   message,
    }
}

对于 errors.Wrap/errors.WithStack 同样如此。

错误处理最佳实践

这里给出一套错误处理的最佳实践。

package main

import (
	"fmt"
	"github.com/pkg/errors"
)

func C() error {
	return errors.New("root error") // 这里返回一个错误,其同时记录了错误消息和错误堆栈
	// return fmt.Errorf("root error") // 这里返回一个错误,但不包含堆栈信息
}

func B() error {
	if err := C(); err != nil {
		// return err                                   // 直接返回错误,不做修改
		// return errors.Wrap(err, "error in B")        // 这里可以选择包装错误,添加消息和堆栈信息
		return errors.WithMessage(err, "error in B") // 这里可以选择只添加消息
		// return errors.WithStack(err)                 // 这里可以选择只添加堆栈信息
	}
	return nil
}

func A() error {
	if err := B(); err != nil {
		// return err                                   // 直接返回错误,不做修改
		// return errors.Wrap(err, "error in A")        // 这里可以选择包装错误,添加消息和堆栈信息
		return errors.WithMessage(err, "error in A") // 这里可以选择只添加消息
		// return errors.WithStack(err)                 // 这里可以选择只添加堆栈信息
	}
	return nil
}

func main() {
	err := A()
	fmt.Printf("%+v\n", err) // %+v 会打印错误消息和错误堆栈,%v 、 %q 和 %s 只打印错误消息
}
  1. 在错误最底层时,使用errors.New,同时记录了错误消息和错误堆栈,方便排查问题。
  2. 在中间层时,使用errors.WithMessage包装或return err直接返回错误,避免堆栈信息过多干扰排查错误日志。
  3. 在最上层时,使用%+v打印,这样可以同时打印错误消息和错误堆栈,方便排查错误日志。

注,如果想采用格式化字符串作为错误消息,可将方法分别替换成:

  • errors.Newerrors.Errorf
  • errors.Wraperrors.Wrapf
  • errors.WithMessageerrors.WithMessagef
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇