SoFunction
Updated on 2025-03-05

Detailed explanation of golang function multiple return value error handling and error type

1. Error type and error value construction

1.1 Error Interface Introduction

In Go,errorType is an interface type, usually used to represent errors. It is defined as follows:

type error interface {
    Error() string
}

errorThere is only one method for the interface, i.e.Error()Method, which returns a string describing the error. This means any implementationError()The types of methods can be used as error types. Usually, functions in Go programs will return aerrorValue of type so that the caller can process or record error information.

1.2 Methods to construct error values

1.2.1 Using the errors package

The designer of Go language provides two conveniencesGoMethods for developers to construct error values:and 。

  • ()Functions are the simplest way to create the error value, which contains only one error message string. This method is suitable for creating simple error values.
  • ()Functions allow you to construct a formatted error message, similar to()Function. This is very useful when you need to build more complex error messages.

Using these two methods, we can easily construct a satisfyingerrorThe error value of the interface is like the following code:

err := ("your first demo error")
errWithCtx = ("index %d is out of bounds", i)

These two methods actually return an instance of the same type that implements the error interface. This unexported type is, its definition is as follows:

// $GOROOT/src/errors/

type errorString struct {
    s string
}

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

In most cases, the error values ​​built using these two methods can meet our needs. But we also need to see that although these two methods of building error values ​​are very convenient,The error context they provide to the error handler is limited to information presented in string form, that is, information returned by the Error method.

1.2.2 Custom error type

In some scenarios, the error handler needs to extract more information from the error value to help him choose the error handling path. Obviously, these two methods cannot be satisfied. At this time, we can customize the error type to meet this requirement. Here is an example:

package main

import "fmt"

// Custom error typetype MyError struct {
	ErrorCode    int
	ErrorMessage string
}

// Error method to implement error interfacefunc (e MyError) Error() string {
	return ("Error %d: %s", , )
}

func someFunction() error {
	// Create custom error values	err := MyError{
		ErrorCode:    404,
		ErrorMessage: "not found",
	}
	return err
}

func main() {
	// Call someFunction to return a custom error value	err := someFunction()
	// Print error message	("mistake:", err)
}

Let's look at another example, such as:netThe package defines an error type that carries an additional error context:

// $GOROOT/src/net/
type OpError struct {
    Op string
    Net string
    Source Addr
    Addr Addr
    Err error
}

In this way, the error handler can choose the error handling path based on the additional context information provided by this type of error value, such as Op, Net, Source, etc., such as the code in the following standard library:

// $GOROOT/src/net/http/
func isCommonNetReadError(err error) bool {
    if err ==  {
        return true
    }
    if neterr, ok := err.(); ok && () {
        return true
    }
    if oe, ok := err.(*); ok &&  == "read" {
        return true
    }
    return false
}

We see that the above code uses type assertions (Type Assertion),judge errorIs the dynamic type of type variable err?*or . iferrThe dynamic type is*, then the type assertion will return the value of this dynamic type (stored inoe), the code can determine itsOpIs the field "read"To determine whether it isCommonNetReadType error.

2. The benefits of error type

2.1 First point: Unified error types

If the code of different developers, code in different projects, and even code in the standard library are uniformlyerrorThe error type presented in the form of interface variables can improve the readability of the code while making it easier to form a unified error handling strategy.

2.2 The second point: The error is the value

The errors we construct are all values, that is, even if we assign the value to the interface type variable of error, we can also make logical comparisons of "==" and "!=" for errors like integer values, and the experience of function callers when they view the error remains unchanged.

Since error is an interface type, the default zero value isnil. So we usually call the error returned by the functionnilCompare to determine whether the function returns an error. If the returned error isnil, means that the function execution is successful, otherwise it means that an error occurred. This convention makes error handling consistent and intuitive. For example, you will often see error judgment codes like the following.

func someFunction() error {
    // Simulate an error situation    return ("This is a mistake")
}

func main() {
    err := someFunction()

    if err != nil {
        ("Function execution failed, error message:", err)
    } else {
        ("Function execution succeeded")
    }
}

2.3 The third point: easy to expand, supports custom error context

Although the errors are uniformly presented in the form of an error interface variable, it is easy for us to extend our error context by customizing the error type, just like the previous Go standard libraryOpErrorType is like that.

The error interface is a contract between the provider of the error value and the inspector of the error value. The implementer of the error interface is responsible for providing the error context for the code responsible for error handling. The decoupling of this specific context of error from the error interface type as the error value type also reflects the concept of "orthogonality" in Go combination design philosophy.

Idiomatic strategies for Go error handling

3.1 Policy 1: Transparent Error Handling Policy

Simply put, error handling in Go is returned based on functions/methods.errorThe process of making decisions and selecting subsequent code execution paths for error value information carried in type variables.

In this way, the simplest error strategy is to completely ignore the specific context information carried by returning the error value, and as long as an error occurs, it will enter the only error processing execution path, such as the following code:

err := doSomething()
if err != nil {
    // Don't care about the specific context information carried by the underlying error value of the err variable    // Execute simple error handling logic and return    ... ...
    return err
}

This isThe most common error handling strategy in GoMore than 80% of Go error handling situations can be classified under this strategy. Under this strategy, since the error handler does not care about the context of the error value, the constructor of the error value (such as the function doSomething above) can directly use the two basic error value constructors provided by the Go standard library.andTo construct the error value, like this:

func doSomething(...) error {
    ... ...
    return ("some error occurred")
}

The context information represented by the error value constructed in this way,It is transparent to error handlers, so this strategy is called a "transparent error handling policy".On the premise that the error handler does not care about the error value context, transparent error handling strategies can minimize the coupling relationship between the error handler and the error value constructor.

3.2 Strategy 2: "Sentinel" error handling strategy

When the error handler cannot make the error handling path selection based on the "transparent error value", the error handler will try to inspect the returned error value, so the following code may appearAnti-mode

data, err := (1)
if err != nil {
    switch () {
    case "bufio: negative count":
        // ... ...
        return
    case "bufio: buffer full":
        // ... ...
        return
    case "bufio: invalid use of UnreadByte":
        // ... ...
        return
    default:
        // ... ...
        return
    }
}

Simply put, the anti-pattern is that the error handler uses the unique context information (a string describing the error) that can be provided by the transparent error value as the basis for the error processing path selection. But this "anti-pattern" will cause seriousImplicit coupling. This means that an inadvertent change in the error description string by the error value constructor will cause changes in the error handler's processing behavior, and the performance of checking the error value through string comparison is also very poor.

So is there any way to do this? The Go standard library uses the method of defining the exported "Sentinel" error value to assist error handlers to inspect the error value and make decisions on the error processing branch, such as the "Sentinel error" defined in the following bufio package:

// $GOROOT/src/bufio/
var (
    ErrInvalidUnreadByte = ("bufio: invalid use of UnreadByte")
    ErrInvalidUnreadRune = ("bufio: invalid use of UnreadRune")
    ErrBufferFull        = ("bufio: buffer full")
    ErrNegativeCount     = ("bufio: negative count")
)

The following code snippet takes advantage of the above sentinel error to make the decision of the error processing branch:

data, err := (1)
if err != nil {
    switch err {
    case :
        // ... ...
        return
    case :
        // ... ...
        return
    case :
        // ... ...
        return
    default:
        // ... ...
        return
    }
}

You can see,The general "Sentinel" error value variable is named in the ErrXXX format. Compared with transparent error strategy, the "Sentinel" strategy allows the error handler to check the error value when it needs to be checked.It can be "targeted".

However, for API developers, exposing the "Sentinel" error value also means that these error values, together with the package's public functions/methods, become part of the API. Once released, developers need to maintain it well. The "Sentinel" error value also makes the error handler who uses these values ​​depend on it.

fromGo version 1.13 beginsThe standard library errors package provides the Is function for error handlers to view error values. The Is function is similar to comparing an error type variable with a "Sentinel" error value, for example, the following code:

// Similar if err == ErrOutOfBounds{ … }if (err, ErrOutOfBounds) {
    // Error handling that goes beyond the boundaries}

The difference is that if the underlying error value of the error type variable is a Wrapped Error, the method will compare it with all wrapped errors on the chain along the error chain (Error Chain) where the wrapping error is located until a matching error is found. Here is an example of the Is function application:

var ErrSentinel = ("the underlying sentinel error")

func main() {
  err1 := ("wrap sentinel: %w", ErrSentinel)
  err2 := ("wrap err1: %w", err1)
    println(err2 == ErrSentinel) //false
  if (err2, ErrSentinel) {
    println("err2 is ErrSentinel")
    return
  }

  println("err2 is not ErrSentinel")
}

In this example, we passfunction, and use%wCreate the wrapping error variables err1 and err2, where err1 implements the wrapping of ErrSentinel, the "sentinel error value", and err2 in turn wraps err1, thus forming an error chain. The one at the top of the error chain is err2, and the one at the bottom is ErrSentinel. After that, we use the value comparison and these two methods to judge the relationship between err2 and ErrSentinel. Run the above code and we will see the following results:

false
err2 is ErrSentinel

We see that after comparing err2 with ErrSentinel by comparison operators, we find that the two are not the same. The function will follow the error chain where err2 is located and find the "Sentinel" error value wrapped to the lowest level downwards.ErrSentinel

If you are usingGo 1.13 and subsequent versions are recommended to use it as much as possibleMethods to check whether an error value is an expected error value, or a specific "Sentinel" error value is packaged.

3.3 Policy 3: Error value type inspection strategy

As we see above, the "Sentinel" error value constructed based on the error value construction method provided by the Go standard library does not provide other valid error context information except that the error handler can "targeted" value comparison. If the error handler needs the error value to provide more "error context", the above error handling strategies and error value construction methods cannot be met.

In this case, we need to provide more "error context" information by customizing the error value of the error type. Moreover, since the error values ​​are presented uniformly through the error interface variable, to obtain the error context information carried by the underlying error type, the error handler needs to use theType Assertion mechanism or Type SwitchI call this error handling methodError value type inspection policy

Let's look at an example in the standard library to deepen our understanding. This json package has a custom one.UnmarshalTypeErrorError type:

// $GOROOT/src/encoding/json/
type UnmarshalTypeError struct {
    Value  string       
    Type    
    Offset int64        
    Struct string       
    Field  string      
}

Error handlers can use error type to view the policy to obtain more error context information for error values. The following is how to use this policy.jsonImplementation of a package method:

// $GOROOT/src/encoding/json/
func (d *decodeState) addErrorContext(err error) error {
    if  != nil || len() > 0 {
        switch err := err.(type) {
        case *UnmarshalTypeError:
             = ()
             = (, ".")
            return err
        }
    }
    return err
}

We see that this code obtains the dynamic type and value represented by the err variable through the type switch statement, and then processes it with the error context information in the matching case branch.

Here, the general custom exported error type isXXXErrorThe form of naming. Like the Sentinel error handling strategy, the error value type viewing strategy has been exposed to the error handler, so these error types become part of the API along with the package's public functions/methods. Once released, developers need to maintain them well. And they also make error handlers who use these types for inspection depend on them.

fromStarting from Go version 1.13, the standard library errors package provides the As function to the error handler to view the error value. As function is similar to judging whether an error type variable is a specific custom error type through type assertion., as shown in the following code:

// Similar to if e, ok := err.(*MyError); ok { … }var e *MyError
if (err, &e) {
    // If the err type is *MyError, the variable e will be set to the corresponding error value}

The difference is that if the dynamic error value of the error type variable is a wrapper error,The function will follow the error chain where the wrapping error is located, and compare it with all the wrapped error types on the chain until a matching error type is found, likeFunctions like that. Below isAsAn example of function application:

type MyError struct {
    e string
}

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

func main() {
    var err = &MyError{"MyError error demo"}
    err1 := ("wrap err: %w", err)
    err2 := ("wrap err1: %w", err1)
    var e *MyError
    if (err2, &e) {
        println("MyError is on the chain of err2")
        println(e == err)                  
        return                             
    }                                      
    println("MyError is not on the chain of err2")
} 

Running the above code will give:

MyError is on the chain of err2
true

We see,Functions alongerr2The error chain is located downwards and the error value that is wrapped to the deepest is found, anderr2and its type* MyErrorSuccessfully matched. After the match is successful, the matched error value will be stored in the second parameter of the As function, which is whyprintln(e == err)OutputtrueThe reason.

If you are usingGo 1.13 and subsequent versions, please try to use the method to check whether an error value is an instance of a custom error type.

3.4 Strategy 4: Error behavior characteristics inspection strategy

I don’t know if you have noticed that among the three strategies we have already mentioned, there is actually only the first one, that is, the "transparent error handling strategy", which effectively reduces the coupling between the error constructor and the error handler. Although the previous strategy 2 and strategy 3 are both effective error handling strategies in our actual encoding, the code using these two strategies still establishes a coupling between the error constructor and the error handler.

So in addition to the "transparent error handling strategy", do we have any means to reduce the coupling between the error handler and the error value constructor?

In the Go standard library, we found such an error handling method:Classify the error types in a certain package, extract some public error behavior characteristics uniformly, and put these error behavior characteristics into a public interface type. This method is also called the wrong behavior feature inspection strategy.

Based on the standard librarynetAs an example, it abstracts and puts the common behavior characteristics of all error types in the package intoIn this interface, as shown in the following code:

// $GOROOT/src/net/
type Error interface {
    error
    Timeout() bool  
    Temporary() bool
}

We see,The interface contains two methods for judging the characteristics of wrong behavior:TimeoutUsed to determine whether it is timeout (Timeout)mistake,TemporaryUsed to determine whether it is temporary (Temporary)mistake.

The error handler only needs to rely on this public interface to view the error behavior characteristic information of specific error values ​​and make decisions on subsequent error processing branch selection based on this information.

Here, let's look at another example of the http package using error behavior feature inspection strategy for error processing to deepen the understanding:

// $GOROOT/src/net/http/
func (srv *Server) Serve(l ) error {
    ... ...
    for {
        rw, e := ()
        if e != nil {
            select {
            case <-():
                return ErrServerClosed
            default:
            }
            if ne, ok := e.(); ok && () {
                // Note: Here are temporary errors                ... ...
                (tempDelay)
                continue
            }
            return e
        }
        ...
    }
    ... ...
}

In the above code,AcceptThe method actually returns an error type *OpError, which isnetA custom error type in the package that implements the error public feature interfaceAs shown in the following code:

// $GOROOT/src/net/
type OpError struct {
    ... ...
    // Err is the error that occurred during the operation.
    Err error
}

type temporary interface {
    Temporary() bool
}

func (e *OpError) Temporary() bool {
  if ne, ok := .(*); ok {
      t, ok := .(temporary)
      return ok && ()
  }
  t, ok := .(temporary)
  return ok && ()
}

Therefore, the OpError instance can be judged by the error handler through the interface method to determine whether its behavior satisfies the Temporary or Timeout characteristics.

4. Summary

The Go language unified error type is the error interface type, and provides a variety of functions that can be quickly built with error values ​​that can be assigned to the error type, including , etc. We also explain the advantages of using unified error as the error type, and you need to understand this deeply.

Based on the Go error handling mechanism, unified error value types and error value construction methods, Go language has formed a variety of idiomatic strategies for error handling, including transparent error handling strategies, "Sentinel" error handling strategies, error value type inspection strategies, and error behavior characteristics inspection strategies. These strategies have applicable occasions, but there is no single error handling strategy that can be suitable for all projects or for all occasions.

In terms of error handling policy selection, you can refer to the following:

  • Please try to use the "transparent error" processing strategy to reduce the coupling between the error handler and the error value constructor;
  • If you can extract common error behavior characteristics from many error types, try to use the "Error behavior characteristics inspection strategy";
  • If the above two strategies cannot be implemented, use the "Sentinel" strategy and the "Error Value Type View" strategy;
  • In Go 1.13 and subsequent versions, try to use it as much as possible.and Function replaces the original error view comparison statement.

The above is a detailed explanation of the detailed information on the error handling and error types of golang functions. For more information about golang error handling, please pay attention to my other related articles!