SoFunction
Updated on 2025-03-04

An article will help you easily understand Golang's error processing

error in Golang

error in Golang is a simple interface type. As long as this interface is implemented, it can be regarded as an error

type error interface {
    Error() string
}

Several ways to play error

Looking through the Golang source code, you can see many error types similar to the following

Sentinel error

var EOF = ("EOF")
var ErrUnexpectedEOF = ("unexpected EOF")
var ErrNoProgress = ("multiple Read calls return no data or error")

shortcoming:

1. Make errors have a duality

error != nil no longer means that an error must have occurred
For example, to return to tell the caller that there is no more data, but this is not an error

2. Create a dependency between two packages

If you use it to check whether all the data has been read, then the io package will be imported into the code

Custom error type

A good example is that it has the advantage of having more context information attached

type PathError struct {
    Op   string
    Path string
    Err  error
}

Wrap error

At this point we can find that Golang's error is very simple, but simplicity also means that sometimes it is not enough.

Golang's error has always had two problems:

No file: line information (that is, no stack information)

For example, this kind of error, if you know which line of the code is reported wrong, it is simply fatal when you debug it.

SERVICE ERROR 2022-03-25T16:32:10.687+0800!!!
       Error 1406: Data too long for column 'content' at row 1

2. When the upper layer error wants to attach more log information, it will often use it.()()A new error will be created, and the underlying error type will be "swallowed"

var errNoRows = ("no rows")

// Imitate the SQL library to return an errNoRowsfunc sqlExec() error {
    return errNoRows
}

func serviceNoErrWrap() error {
    err := sqlExec()
    if err != nil {
        return ("sqlExec :%v", err)
    }
    
    return nil
}

func TestErrWrap(t *) {
    // Created a new err with missing the underlying err    err := serviceNoErrWrap()
    if err != errNoRows {
        ("===== errType don't equal errNoRows =====")
    }
}
-------------------------------Code running results----------------------------------
=== RUN   TestErrWrap
2022/03/26 17:19:43 ===== errType don't equal errNoRows =====

To solve this problem, we can use/pkg/error package,use()methodKeep err
Save towithStack object

// The withStack structure saves an error, forming an error chain.  At the same time, the *stack field saves the stack information.type withStack struct {
    error
    *stack
}

Can also be used(err, "custom text"), with some custom text information

Source code interpretation: first package err and messagewithMessage object, thenwithMessage objectand stack information packagewithStack object

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

New features of Golang version 1.13 error

Golang 1.13 version borrowed from/pkg/error package, the following functions have been added, greatly enhancing the ability of Golang language to judge error types

()

// Contrary to () behavior// Get the underlying err in the err chainfunc Unwrap(err error) error {
    u, ok := err.(interface {
        Unwrap() error
    })
    if !ok {
        return nil
    }
    return ()
}

()

Before version 1.13, we can useerr == targetErrDetermine err type
()It's its enhanced version: error chaineither err == targetErr,Right nowreturn true

// Practice: Learn to use()var errNoRows = ("no rows")

// Imitate the SQL library to return an errNoRowsfunc sqlExec() error {
    return errNoRows
}

func service() error {
    err := sqlExec()
    if err != nil {
        return (err)    // Package errNoRows    }
    
    return nil
}

func TestErrIs(t *) {
    err := service()
    
    // Recursively call, hit any err on the err chain will return true    if (err, errNoRows) {
        ("===== () succeeded =====")
    }
    
    // Err is packaged and cannot be judged by ==    if err == errNoRows {
        ("err == errNoRows")
    }
}
-------------------------------Code running results----------------------------------
=== RUN   TestErrIs
2022/03/25 18:35:00 ===== () succeeded =====

Example interpretation:

Because of usePackedsqlErrorsqlErrorLocated at the bottom of the error chain, the upper error is no longersqlErrorType, so use==Can't tell the bottomsqlError

Source code interpretation:

  • It's easy to think of its internal callserr = Unwrap(err)Method to get the error at the bottom of the error chain
  • Custom error types can be implementedIs interfaceCustomize error type judgment method
func Is(err, target error) bool {
    if target == nil {
        return err == target
    }
    
    isComparable := (target).Comparable()
    for {
        if isComparable && err == target {
            return true
        }
        // Support custom error type judgment        if x, ok := err.(interface{ Is(error) bool }); ok && (target) {
            return true
        }
        if err = Unwrap(err); err == nil {
            return false
        }
    }
}

Let's take a look at how to customize error type judgment:

CustomerrNoRows type, it must be implementedIs interface, to use()Make type judgment

type errNoRows struct {
    Desc string
}

func (e errNoRows) Unwrap() error { return e }

func (e errNoRows) Error() string { return  }

func (e errNoRows) Is(err error) bool {
    return (err).Name() == (e).Name()
}

// Imitate the SQL library to return an errNoRowsfunc sqlExec() error {
    return &errNoRows{"Kaolengmian NB"}
}

func service() error {
    err := sqlExec()
    if err != nil {
        return (err)
    }
    
    return nil
}

func serviceNoErrWrap() error {
    err := sqlExec()
    if err != nil {
        return ("sqlExec :%v", err)
    }
    
    return nil
}

func TestErrIs(t *) {
    err := service()
    
    if (err, errNoRows{}) {
        ("===== () succeeded =====")
    }
}
-------------------------------Code running results----------------------------------
=== RUN   TestErrIs
2022/03/25 18:35:00 ===== () succeeded =====

()

Before version 1.13, we can useif _,ok := err.(targetErr)Determine err type
()It's its enhanced version: error chainAny err is the same as targetErr,Right nowreturn true

// Learn to use() through examplestype sqlError struct {
    error
}

func (e *sqlError) IsNoRows() bool {
    t, ok := .(ErrNoRows)
    return ok && ()
}

type ErrNoRows interface {
    IsNoRows() bool
}

// Return a sqlErrorfunc sqlExec() error {
    return sqlError{}
}

// Package sqlErrorfunc service() error {
    err := sqlExec()
    if err != nil {
        return (err)
    }
    
    return nil
}

func TestErrAs(t *) {
    err := service()
    
    // Use recursively, as long as there is an Err on the Err chain satisfying the type assertion, it returns true    sr := &sqlError{}
    if (err, sr) {
        ("===== () succeeded =====")
    }
    
    // After packaging, the current Err cannot be converted to the underlying Err through type assertion    if _, ok := err.(sqlError); ok {
        ("===== type assert succeeded =====")
    }
}
----------------------------------Code running results--------------------------------------------
=== RUN   TestErrAs
2022/03/25 18:09:02 ===== () succeeded =====

Example interpretation:

Because of usePackedsqlErrorsqlErrorLocated at the bottom of the error chain, the upper error is no longersqlErrorType, so using type assertions cannot determine the underlyingsqlError

error handling best practices

The above talks about how to define the error type and how to compare the error type. Now let's talk about how to do error processing in large projects.

Priority to error

When a function returns a non-empty error, the error should be processed first, and its other return values ​​should be ignored

Only handle error once

  • In Golang, for each err we should only handle it once.
  • Either immediately handle err (including logging and other behaviors) and return nil (swallow the error). At this time, because the error has been downgraded, be careful to handle the function return value.

For example, the following example (conf) does not have return err, so be careful of errors such as null pointers when using buf.

Or return err, process err on the upper layer

Counterexample:

// Imagine if the writeAll function errors, the log will be printed twice// If the entire project does this, you will be surprised to find that we are logging everywhere, and there are a lot of valuable garbage logs in the project.// unable to write:
// could not write config:

type config struct {}

func writeAll(w , buf []byte) error {
    _, err := (buf)
    if err != nil {
        ("unable to write:", err)
        return err
    }
    
    return nil
}

func writeConfig(w , conf *config) error {
    buf, err := (conf)
    if err != nil {
        ("could not marshal config:%v", err)
    }
    
    if err := writeAll(w, buf); err != nil {
        ("count not write config: %v", err)
        return err
    }
    
    return nil
}

Don't wrap error repeatedly

We should package error, but only once

Upper level business code suggestionsWrap error, but the underlying basic Kit library is not recommended

If the underlying basic Kit library is packaged once and the upper business code is packaged once again, the error is packaged repeatedly, and the log will be re-blown

For example, what we often usesql libraryWill returnThis predefined error, instead of giving us a wrapped error

Opacity error handling

In large projects, it is recommended to useOpacity errors: Don't care about the error type, just care whether the error is nil

benefit:

The coupling is small, so there is no need to judge a specific error type, so there is no need to import the dependencies of related packages.
However, sometimes, this way of handling error is not enough, such as: the business needs to be correctParameter exception error typeDegrading and printing Warn-level logs

type ParamInvalidError struct {
    Desc string
}

func (e ParamInvalidError) Unwrap() error { return e }

func (e ParamInvalidError) Error() string { return "ParamInvalidError: " +  }

func (e ParamInvalidError) Is(err error) bool {
    return (err).Name() == (e).Name()
}

func NewParamInvalidErr(desc string) error {
    return (&ParamInvalidError{Desc: desc})
}
------------------------------Top-level log printing---------------------------------
if (err, {}) {
    (ctx, "%s", ())
    return
}
if err != nil {
    (ctx, " error:%+v", err)
}

Simplify error handling

Golang because of countlessif err != nilCriticized, now let's see how to reduce itif err != nilThis code

CountLines() implements the "number of rows to read content" function

The error can be simplified by using ():

func CountLines(r ) (int, error) {
    var (
        br    = (r)
        lines int
        err   error
    )
    
    for {
        _, err := ('\n')
        lines++
        if err != nil {
            break
        }
    }
    
    if err !=  {
        return 0, nilsadwawa 
    }
    
    return lines, nil
}

func CountLinesGracefulErr(r ) (int, error) {
    sc := (r)
    
    lines := 0
    for () {
        lines++
    }
    
    return lines, ()
}

()Return oneScannerObject, the structure contains the error type, and the callErr()Method can return the encapsulated error

Golang source code contains a lot of excellent design ideas. We learn from it when reading the source code and use it in practice.

type Scanner struct {
    r             // The reader provided by the client.
    split        SplitFunc // The function to split the tokens.
    maxTokenSize int       // Maximum size of a token; modified by tests.
    token        []byte    // Last token returned by split.
    buf          []byte    // Buffer used as argument to split.
    start        int       // First non-processed byte in buf.
    end          int       // End of data in buf.
    err          error     // Sticky error.
    empties      int       // Count of successive empty tokens.
    scanCalled   bool      // Scan has been called; buffer is in use.
    done         bool      // Scan has finished.
}

func (s *Scanner) Err() error {
    if  ==  {
        return nil
    }
    return 
}

errWriter

WriteResponse()The function is implemented"Build HttpResponse"Function

Using the ideas learned above, we can implement one by ourselveserrWriterObject, simplifies the processing of error

type Header struct {
    Key, Value string
}

type Status struct {
    Code   int
    Reason string
}

func WriteResponse(w , st Status, headers []Header, body ) error {
    _, err := (w, "HTTP/1.1 %d %s\r\n", , )
    if err != nil {
        return err
    }
    
    for _, h := range headers {
        _, err := (w, "%s: %s\r\n", , )
        if err != nil {
            return err
        }
    }
    
    if _, err := (w, "\r\n"); err != nil {
        return err
    }
    
    _, err = (w, body)
    return err
}

type errWriter struct {
    
    err error
}

func (e *errWriter) Write(buf []byte) (n int, err error) {
    if  != nil {
        return 0, 
    }
    
    n,  = (buf)
    
    return n, nil
}

func WriteResponseGracefulErr(w , st Status, headers []Header, body ) error {
    ew := &errWriter{w, nil}
    
    (ew, "HTTP/1.1 %d %s\r\n", , )
    
    for _, h := range headers {
        (ew, "%s: %s\r\n", , )
    }
    
    (w, "\r\n")
    
    (ew, body)
    
    return 
}

When should I use panic

In GolangpanicIt will cause the program to exit directly, which is a fatal error.

It is recommended to use panic only when a fatal program error occurs, such as index out-of-bounds, irrecoverable environmental problems, stack overflow, etc.

Small addition

()The return iserrorString objectThe reason for pointer to prevent string collisions. If a collision occurs, the two error objects will be equal.
Source code:

func New(text string) error {
    return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return 
}

practice:error1anderror2All texts"error", but the two are not equal

func TestErrString(t *) {
    var error1 = ("error")
    var error2 = ("error")
    
    if error1 != error2 {
        ("error1 != error2")
    }
}
---------------------Code running results--------------------------
=== RUN   TestXXXX
2022/03/25 22:05:40 error1 != error2

References
《Effective GO》
"Go Programming Language"
/practical-go/presentations/#_error_handling

Summarize

This is all about this article about error processing in Golang. For more information about error processing in Golang, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!