In the back-end services developed with Go, there have been many different solutions for error handling. This article discusses and proposes a complete solution for error transmission, return and backtracking from within the service to outside the service. Readers are also invited discuss together.

Questions raised

In the background development, there are three dimensions of problems that need to be solved for error handling:

  • Error handling inside the function: This refers to the error handling when a function encounters various errors during execution. This is a language-level problem
  • Function/module error message return: How can a function return this error message gracefully after an operation error, so that the caller can handle it (and gracefully). This is also a language-level problem
  • Service/system error message return: How to return a friendly error message when the microservice/system fails in processing still needs to be understood and handled gracefully by the caller. This is a service level issue, applicable to any language

In response to these three dimensions, the author is going to write three articles explaining them one by one. First of all, this article is the first one: Error handling inside the

High-level language error handling mechanism

A procedure-oriented function requires different handles of different error messages in different processes; an object-oriented function may require different processing for different types of errors returned by an operation. In addition, when you encounter an error, you can also use the method of assertion to quickly terminate the function process, which greatly improves the readability of the code.

try ... catch is provided in many high-level languages, and a unified error handling logic can be realized through this scheme inside the function. Even C the "intermediate language" of try catch does not have 061557ca2ecbd1, programmers can also use macro definitions with goto LABEL to achieve a certain degree of error assertion and handling.

Go's false assertions

The situation in Go is more embarrassing. Let's look at the assertion first. Our goal is to be able to check for errors and terminate the current function with just one line of code. Since there is no throw and no macros, there are two ways to implement a one-line assertion.

Method 1: Single line if + return

The first is to if the error judgment of 061557ca2ecc23 in one line, such as:

    if err != nil { return err }

This method has some debatable points:

  • Although it conforms to Go's code specifications, in practice, the fact that the curly braces in the if statement does not wrap is still very controversial, and the author rarely sees it in the actual code.
  • The code is not intuitive enough. When browsing the code roughly, the assertion code is not conspicuous, and return . The reason is that the Go specification strongly discourages the use of ; to separate multiple statements ( if condition Except for judgment)

Therefore, the author strongly does not recommend this.

Method 2: panic + recover

The second method is to borrow the panic and combine it with recover , as shown in the following code:

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))

    // ...
}

Is this method good? We have to look at each situation

First of all, panic was originally designed to be called when the program or coroutine encounters a serious error and cannot continue to run at all (such as segfault, shared resource competition error_). This is equivalent to FATAL level in Linux. With this mechanism, it is only used for ordinary error handling ( ERROR ).

Secondly, the panic call itself has a relatively large system overhead compared to ordinary business logic. The error handling of such things may be normalized logic. Frequent panic - recover operations will also greatly reduce the throughput of the system.

But that being said, although the scheme of using panic to assert is basically not used in business logic, it is very common in test scenarios. What about testing? A slightly larger system overhead is fine. For Go, the very popular unit testing framework goconvey uses the panic mechanism to implement the assertion in the unit test, and everyone who uses it is good.

Conclusion suggestion

To sum up, in Go, the author does not recommend using assertions for business code, and it is recommended to use this format honestly when encountering errors:

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

In the single test code, you can use panic-based assertions such as goconvey

Go's try ... catch

As we all know, Go (current version 1.17) does not have try ... catch , and from the official attitude, there is no clear plan in a short time. But programmers have this demand. Here also gave birth to a centralized solution

defer function

The method used by the author is to err variable that needs to be returned within the function, and then combine it with defer to process it uniformly:

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
    }

    // ...
}

This kind of scheme should pay special attention to the problem of variable scope.

For example, in the if err = DoSomething(); err != nil { line 061557ca2eced0, if we change err = ... to err := ... err variable in this line and the 061557ca2eced4 defined at the (err error) function are not the same variable, so even if an error occurs here, it cannot be captured in the defer function err variable.

try ... catch , the author actually does not have a particularly good way to simulate, even the above method has a very troublesome problem: the defer writing method leads to the error processing front, and the normal logic rear.

Named error handling function

To solve the problem of pre-error handling caused by the writing of defer mentioned above, the first solution is more conventional, that is, to change the anonymous function behind defer to a named function, abstracting a special error handling function. At this time, we can transform the previous function in this way:

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
    }
}

This is more comfortable, at least before the logic, and after the error handling. But readers will definitely find out-isn't it possible to do this in any language? Admittedly, this doesn't look like try ... catch , but this method is still very recommended, especially when the error handling code is very long.

goto LABEL

Theoretically, we can goto the error handling through the 061557ca2ecf7b statement, such as:

func SomeProcess() error {
    // ...

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

    // ...

    return nil

ERR:
    // ...
}

C language may find it very cordial, because there are a lot of such writing methods in the Linux kernel. The author can't tell the specific bad points about this writing method, but this writing method that looks a lot like C, in fact, has many restrictions. On the contrary, compared with C, there are more places to pay attention to:

  • Limited to ANSI-C, all local variables are required to be forward-declared, which avoids variable coverage with the same name due to variable scope; but Go needs to pay attention to this problem.
  • C supports macro definitions, and can realize assertions in conjunction with the previous article, making error handling statements more elegant; Go does not support
  • Go often has many anonymous functions. Anonymous functions cannot goto to the outer function label, which also limits the use goto

However, the author does not support the use of goto . I just think that under the existing mechanism, it is more in line with Go's habits to use the first two modes.


The next article is "How to gracefully handle and return errors in Go (2) error wrapping /module error message return", the author has sorted out the 061557ca2ed05b functions after Go 1.13 in detail, so stay tuned~~


This article uses the Creative Commons Attribution-Non-Commercial Use-Same Method Sharing 4.0 International License Agreement for licensing.

This article was first published in the cloud + community , which is also the blog amc

Original author: amc , welcome to reprint, but please indicate the source.

Original title: "How to gracefully handle and return errors in Go (1)-Error handling inside functions"

Release Date: 2021-09-30

Original link: https://segmentfault.com/a/1190000040762538 .


amc
924 声望223 粉丝

电子和互联网深耕多年,拥有丰富的嵌入式和服务器开发经验。现负责腾讯心悦俱乐部后台开发