Best practice Golang error handling

1. Native error handling

Go language provides a very simple error handling mechanism through the built-in error interface.
error type is an interface type. This is its definition:
type error interface {
    Error() string
}

We can generate error information by implementing the error interface type in coding.
Functions usually return error information in the last return value. Use errors.New to return an error message:

func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, errors.New("math: square root of negative number")
    }
    // realization
}

In the following example, we pass a negative number when calling Sqrt, and then we get the non nil error object. Comparing this object with nil, the result is true, so fmt.Println(fmt package will call the error method when processing error) is called to output the error. See the example code called below:

result, err:= Sqrt(-1)
if err != nil {
   fmt.Println(err)
}

2. Open source error package

github.com/pkg/errors package adds the following common functions to the original error package:

  • You can print the stack information of error: the print error needs% + v to be output in detail
  • Use Wrap or Wrapf to initialize an error
  • Using errors.WithMessage, you can wrap another layer based on the original error, including the original error information
  • errors.Is, which is used to judge the error type. Different processing can be done according to different error types
  • errors.As, used to resolve error s

See the global error handling section for specific use cases.

3. Error handling in Engineering

3.1 requirements sorting

  • Customize the error information and code it

    • The controller layer can determine the user-defined error type, and finally determine whether to process by info or error
  • You can print the initial location of the error (get the call stack of the error)
  • Confirm the current system location:

    • User, get TagMessage
    • Upstream service, error code mapping required
    • Log monitoring, TagMessage monitoring

Next, in an engineering project, a complete set of error handling mechanism is implemented by using github.com/pkg/errors package

3.2 method 1: Map saves the mapping between error code and Message

3.2.1 definition error message

New error_handler.go

package error_handle

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

// 1. Customize the error structure and override the Error() method
// Custom structure returned on error
type CustomError struct {
    Code       int    `json:"code"`    // Service code
    TagMessage string `json:"message"` // Description information
}

func (e *CustomError) Error() string {
    return e.TagMessage
}

// 2. Define errorCode
const (
    // Service level error code
    ServerError        = 10101
    TooManyRequests    = 10102
    ParamBindError     = 10103
    AuthorizationError = 10104
    CallHTTPError      = 10105
    ResubmitError      = 10106
    ResubmitMsg        = 10107
    HashIdsDecodeError = 10108
    SignatureError     = 10109

    // Business module level error code
    // User module
    IllegalUserName = 20101
    UserCreateError = 20102
    UserUpdateError = 20103
    UserSearchError = 20104

    // Authorized caller
    AuthorizedCreateError    = 20201
    AuthorizedListError      = 20202
    AuthorizedDeleteError    = 20203
    AuthorizedUpdateError    = 20204
    AuthorizedDetailError    = 20205
    AuthorizedCreateAPIError = 20206
    AuthorizedListAPIError   = 20207
    AuthorizedDeleteAPIError = 20208

    // administrators
    AdminCreateError             = 20301
    AdminListError               = 20302
    AdminDeleteError             = 20303
    AdminUpdateError             = 20304
    AdminResetPasswordError      = 20305
    AdminLoginError              = 20306
    AdminLogOutError             = 20307
    AdminModifyPasswordError     = 20308
    AdminModifyPersonalInfoError = 20309

    // to configure
    ConfigEmailError        = 20401
    ConfigSaveError         = 20402
    ConfigRedisConnectError = 20403
    ConfigMySQLConnectError = 20404
    ConfigMySQLInstallError = 20405
    ConfigGoVersionError    = 20406

    // Practical toolbox
    SearchRedisError = 20501
    ClearRedisError  = 20502
    SearchRedisEmpty = 20503
    SearchMySQLError = 20504

    // menu bar
    MenuCreateError = 20601
    MenuUpdateError = 20602
    MenuListError   = 20603
    MenuDeleteError = 20604
    MenuDetailError = 20605

    // Borrow books
    BookNotFoundError        = 20701
    BookHasBeenBorrowedError = 20702
)

// 3. Define the text information corresponding to errorCode
var codeTag = map[int]string{
    ServerError:        "Internal Server Error",
    TooManyRequests:    "Too Many Requests",
    ParamBindError:     "Incorrect parameter information",
    AuthorizationError: "Incorrect signature information",
    CallHTTPError:      "Call third party HTTP Interface failure",
    ResubmitError:      "Resubmit Error",
    ResubmitMsg:        "Do not resubmit",
    HashIdsDecodeError: "ID Parameter error",
    SignatureError:     "SignatureError",

    IllegalUserName: "Illegal user name",
    UserCreateError: "Failed to create user",
    UserUpdateError: "Failed to update user",
    UserSearchError: "Failed to query user",

    AuthorizedCreateError:    "Failed to create caller",
    AuthorizedListError:      "Failed to get caller list page",
    AuthorizedDeleteError:    "Failed to delete caller",
    AuthorizedUpdateError:    "Failed to update caller",
    AuthorizedDetailError:    "Failed to get caller details",
    AuthorizedCreateAPIError: "Create caller API Address failure",
    AuthorizedListAPIError:   "Get caller API Address list failed",
    AuthorizedDeleteAPIError: "Delete caller API Address failure",

    AdminCreateError:             "Failed to create administrator",
    AdminListError:               "Failed to get administrator list page",
    AdminDeleteError:             "Failed to delete administrator",
    AdminUpdateError:             "Failed to update administrator",
    AdminResetPasswordError:      "Failed to reset password",
    AdminLoginError:              "Login failed",
    AdminLogOutError:             "Exit failed",
    AdminModifyPasswordError:     "Failed to modify password",
    AdminModifyPersonalInfoError: "Failed to modify personal information",

    ConfigEmailError:        "Failed to modify mailbox configuration",
    ConfigSaveError:         "Failed to write configuration file",
    ConfigRedisConnectError: "Redis connection failed",
    ConfigMySQLConnectError: "MySQL connection failed",
    ConfigMySQLInstallError: "MySQL Failed to initialize data",
    ConfigGoVersionError:    "GoVersion Not meeting requirements",

    SearchRedisError: "query RedisKey fail",
    ClearRedisError:  "empty RedisKey fail",
    SearchRedisEmpty: "Inquired RedisKey non-existent",
    SearchMySQLError: "query mysql fail",

    MenuCreateError: "Failed to create menu",
    MenuUpdateError: "Failed to update menu",
    MenuDeleteError: "Failed to delete menu",
    MenuListError:   "Failed to get menu list page",
    MenuDetailError: "Failed to get menu details",

    BookNotFoundError:        "Book not found",
    BookHasBeenBorrowedError: "The book has been borrowed",
}

func Text(code int) string {
    return codeTag[code]
}

// 4. New custom error instantiation
func NewCustomError(code int) error {
    // The first call must be instantiated with the Wrap method
    return errors.Wrap(&CustomError{
        Code:       code,
        TagMessage: codeTag[code],
    }, "")
}

3.3 user defined Error usage

New test file: error_handler_test.go

package error_handle

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

func TestText(t *testing.T) {
    books := []string{
        "Book1",
        "Book222222",
        "Book3333333333",
    }

    for _, bookName := range books {
        err := searchBook(bookName)

        // Special business scenario: if the book is found to be borrowed, just come back next time. It does not need to be treated as an error
        if err != nil {
            // The error code at the bottom of the interface, error, is usually extracted before the API returns
            // As - get the specific implementation of the error
            var myError = new(CustomError)
            // As - parse error content
            if errors.As(err, &myError) {
                fmt.Printf("AS Information in: the current book is: %s ,error code is %d, message is %s\n", bookName, myError.Code, myError.TagMessage)
            }

            // For special scenarios, when an error (ErrorBookHasBeenBorrowed) is specified, it can be printed without returning an error
            // Is - determines whether the error is of the specified type
            if errors.Is(err,  NewCustomError(BookHasBeenBorrowedError)) {
                fmt.Printf("IS Information in:%s It has been borrowed, Just press Info handle!\n", bookName)
                err = nil
            }else {
                // If there is stack information, call the WithMessage method
                newErr := errors.WithMessage(err, "WithMessage err")
                fmt.Printf("IS Information in:%s Not found, should press Error handle! ,newErr is %s\n", bookName , newErr)
            }
        }
    }
}

func searchBook(bookName string) error {
    // 1. It is found that the book does not exist in the Library - it is considered to be an error and detailed error information needs to be printed
    if len(bookName) > 10 {
        return NewCustomError(BookHasBeenBorrowedError)
    } else if len(bookName) > 6 {
        // 2. If you find that the book has been borrowed - just print the prompt of being taken away. It is not considered an error
        return NewCustomError(BookHasBeenBorrowedError)
    }
    // 3 find the book - no processing required
    return nil
}

3.3 method 2: simplify the code with the help of generate (recommended)

The relationship between maintenance error codes and error information in mode 1 is complex. We can automatically generate codes with the help of go generate.

3.3.1 installing stringer

stringer is not a Go built-in tool and needs to be installed manually. Execute the following commands

go get golang.org/x/tools/cmd/stringer

3.3.1 definition error message

New error_handler.go. In error_ In handler, add a comment / / go:generate stringer -type ErrCode -linecomment. Execute go generate to generate a new file

package error_handle

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

// 1. Customize the error structure and override the Error() method
// Custom structure returned on error
type CustomError struct {
    Code    ErrCode `json:"code"`    // Service code
    Message string  `json:"message"` // Service code
}

func (e *CustomError) Error() string {
    return e.Code.String()
}

type ErrCode int64 //Error code

// 2. Define errorCode
//go:generate stringer -type ErrCode -linecomment
const (
    // Service level error code
    ServerError        ErrCode = 10101 // Internal Server Error
    TooManyRequests    ErrCode = 10102 // Too Many Requests
    ParamBindError     ErrCode = 10103 // Incorrect parameter information
    AuthorizationError ErrCode = 10104 // Incorrect signature information
    CallHTTPError      ErrCode = 10105 // Failed to call the third-party HTTP interface
    ResubmitError      ErrCode = 10106 // ResubmitError
    ResubmitMsg        ErrCode = 10107 // Do not resubmit
    HashIdsDecodeError ErrCode = 10108 // Error in ID parameter
    SignatureError     ErrCode = 10109 // SignatureError

    // Business module level error code
    // User module
    IllegalUserName ErrCode = 20101 // Illegal user name
    UserCreateError ErrCode = 20102 // Failed to create user
    UserUpdateError ErrCode = 20103 // Failed to update user
    UserSearchError ErrCode = 20104 // Failed to query user

    // to configure
    ConfigEmailError        ErrCode = 20401 // Failed to modify mailbox configuration
    ConfigSaveError         ErrCode = 20402 // Failed to write configuration file
    ConfigRedisConnectError ErrCode = 20403 // Redis connection failed
    ConfigMySQLConnectError ErrCode = 20404 // MySQL connection failed
    ConfigMySQLInstallError ErrCode = 20405 // MySQL failed to initialize data
    ConfigGoVersionError    ErrCode = 20406 // GoVersion does not meet requirements

    // Practical toolbox
    SearchRedisError ErrCode = 20501 // Failed to query RedisKey
    ClearRedisError  ErrCode = 20502 // Failed to clear RedisKey
    SearchRedisEmpty ErrCode = 20503 // RedisKey queried does not exist
    SearchMySQLError ErrCode = 20504 // Failed to query mysql

    // menu bar
    MenuCreateError ErrCode = 20601 // Failed to create menu
    MenuUpdateError ErrCode = 20602 // Failed to update menu
    MenuListError   ErrCode = 20603 // Failed to delete menu
    MenuDeleteError ErrCode = 20604 // Failed to get menu list page
    MenuDetailError ErrCode = 20605 // Failed to get menu details

    // Borrow books
    BookNotFoundError        ErrCode = 20701 // Book not found
    BookHasBeenBorrowedError ErrCode = 20702 // The book has been borrowed
)

// 4. New custom error instantiation
func NewCustomError(code ErrCode) error {
    // The first call must be instantiated with the Wrap method
    return errors.Wrap(&CustomError{
        Code:    code,
        Message: code.String(),
    }, "")
}

3.3.2 user defined Error usage

New test file: error_handler_test.go

package error_handle

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

func TestText(t *testing.T) {
    books := []string{
        "Book1",
        "Book222222",
        "Book3333333333",
    }

    for _, bookName := range books {
        err := searchBook(bookName)

        // Special business scenario: if the book is found to be borrowed, just come back next time. It does not need to be treated as an error
        if err != nil {
            // The error code at the bottom of the interface, error, is usually extracted before the API returns
            // As - get the specific implementation of the error
            var customErr = new(CustomError)
            // As - parse error content
            if errors.As(err, &customErr) {
                //FMT. Printf ("information in as: current book is:% s, error code is% D, message is% s \ n", bookname, customerr. Code, customerr. Message)
                if customErr.Code == BookHasBeenBorrowedError {
                    fmt.Printf("IS Medium info Information:%s It has been borrowed, Just press Info handle!\n", bookName)
                } else {
                    // If there is stack information, call the WithMessage method
                    newErr := errors.WithMessage(err, "WithMessage err1")
                    // Use% + v to print the complete stack information
                    fmt.Printf("IS Medium error Information:%s Not found, should press Error handle! ,newErr is: %+v\n", bookName, newErr)
                }
            }
        }
    }
}

func searchBook(bookName string) error {
    // 1. It is found that the book does not exist in the Library - it is considered to be an error and detailed error information needs to be printed
    if len(bookName) > 10 {
        return NewCustomError(BookNotFoundError)
    } else if len(bookName) > 6 {
        // 2. If you find that the book has been borrowed - just print the prompt of being taken away. It is not considered an error
        return NewCustomError(BookHasBeenBorrowedError)
    }
    // 3 find the book - no processing required
    return nil
}

4 Summary

  1. As the bottom implementation of global error, CustomError saves specific error codes and error information;
  2. When CustomError returns an error upward, first initialize the stack with Wrap, and then increase the stack information with WithMessage;
  3. When parsing a specific error from error, use errors.As to extract CustomError, and the error code and error information can be passed into the specific API interface;
  4. To determine whether the error is the specified error, use the methods of errors.Is + Handler Error to process the logic in some specific cases;

Tips:

  1. Don't always use errors.Wrap to wrap errors repeatedly. The stack information will explode. You can test and understand the specific situation by yourself
  2. Using go generate can greatly simplify the repeated work of initializing Erro
  3. github.com/pkg/errors is fully compatible with the error of the standard library. You can replace and later transform the code left over by history
  4. It must be noted that the stack of printing error needs to use% + V, while the original% v is still an ordinary string method; At the same time, pay attention to whether the log collection tool supports multi line matching

I am Jian Fan, an inspirational migrant worker in the new era who aims to describe the most complex problems in the simplest language. If you have any doubts about this article, please leave a message in my WeChat official account. What I can do is to help you:

  • Help build your own knowledge system
  • Real high concurrency scenarios on the Internet
  • Occasionally share classic scenarios and practices related to Golang and Java

My blog: https://besthpt.github.io/
The official account of WeChat:

Tags: Go Back-end

Posted on Sat, 06 Nov 2021 00:11:21 -0400 by nolos