In practical engineering projects, problems are always quickly located through the error information of the program, but you do not want to write redundant and verbose error handling code. Go does not provide a try like Java, C#… Instead, the function returns a value that is thrown up a level by level. This design encourages explicit error checking rather than ignoring errors in the code, which has the benefit of avoiding errors that should be handled. But there’s a downside, which is redundancy.

What is wrong

A mistake is when something goes wrong where it could go wrong. This is expected if you fail to open a file.

And an exception is when something goes wrong where it shouldn’t go wrong. References to null Pointers, for example, are unexpected. As you can see, errors are part of the business process, while exceptions are not.

Errors in Go are another type. Errors are represented by the built-in error type. Like other types, such as int, float64,. Error values can be stored in variables, returned from functions, and so on.

Presentation error

Try to open a file that does not exist:

func main(a) {  
    f, err := os.Open("/test.txt")
    iferr ! =nil {
        fmt.Println(err)
        return
    }
  // Read or write files according to f
    fmt.Println(f.Name(), "opened successfully")}Copy the code

There are functions to open files in the OS package:

func Open(name string) (file *File, err error)
Copy the code

If the file has been successfully opened, the Open function returns file processing. If an error occurs while opening the file, a non-nil error is returned.

If a function or method returns an error, then by convention it must be the last value returned by the function. Therefore, the value returned by the Open function is the last value.

The conventional way to handle errors is to compare returned errors to nil. A nil value indicates that no error occurred, while a non-nil value indicates that an error occurred.

Running results:

open /test.txt: No such file or directory
Copy the code

You get an error indicating that the file does not exist.

Error type representation

The Go language provides a very simple error handling mechanism through a built-in error interface.

It is very simple, with only one Error method to return the specific Error message:

type error interface {
    Error() string
}
Copy the code

It contains a method with an Error() string. Any type that implements this interface can be used as an error. This method provides a description of the error.

When an Error is printed, the fmt.println function internally calls the Error() method to get a description of the Error. This is how the error description is printed on one line.

Different ways to extract more information from errors

In the example above, the wrong description was simply printed. If you want the actual path to the file that caused the error. One possible approach is to parse the error string,

open /test.txt: No such file or directory  
Copy the code

You can parse the error message and get the file path “/test.txt” from it. But this is a bad approach. In a new version of the language, the error description can change at any time, and the code will break.

The standard Go library provides more information about errors in a different way.

Asserts the underlying structure type, structure field fetch

If you look closely at the documentation for the open function, you can see that it returns an error of type PathError. PathError is a struct type that is implemented in the standard library as follows,

type PathError struct {  
    Op   string
    Path string
    Err  error
}

func (e *PathError) Error(a) string { return e.Op + "" + e.Path + ":" + e.Err.Error() }  
Copy the code

From the above code, you can understand that PathError implements the Error interface by declaring the Error() String method. This method connects the operation, path, and actual error and returns it. So you get an error message,

open /test.txt: No such file or directory 
Copy the code

The path field of the PathError structure contains the path to the file that caused the error. Modify the above example and print the path:

func main(a) {  
    f, err := os.Open("/test.txt")
    if err, ok := err.(*os.PathError); ok {
        fmt.Println("File at path", err.Path, "failed to open")
        return
    }
    fmt.Println(f.Name(), "opened successfully")}Copy the code

Use type assertions to get the base value of the wrong interface. Then print the path with errors. This program says,

File at path /test.txt failed to open  
Copy the code

Asserts the underlying structure type, obtained using the method

The second way to get more information is to assert the underlying type and get more information by calling a method of the struct type:

type DNSError struct{... }func (e *DNSError) Error(a) string{... }func (e *DNSError) Timeout(a) bool{... }func (e *DNSError) Temporary(a) bool{... }Copy the code

As you can see from the above code, DNSError struct has two methods Timeout() bool and Temporary() bool, which return a Boolean indicating whether the error is due to Timeout or Temporary.

Write a program that asserts the *DNSError type and calls these methods to determine whether the error is temporary or timed out.

func main(a) {  
    addr, err := net.LookupHost("golangbot123.com")
    if err, ok := err.(*net.DNSError); ok {
        if err.Timeout() {
            fmt.Println("operation timed out")}else if err.Temporary() {
            fmt.Println("temporary error")}else {
            fmt.Println("generic error: ", err)
        }
        return
    }
    fmt.Println(addr)
}
Copy the code

In the above program, try to get the IP address of an invalid domain name by declaring it by typing * net.dnserror to get the potential value of the error.

In the example, the error is neither temporary nor due to a timeout, so the program prints:

generic error:  lookup golangbot123.com: no such host  
Copy the code

If the error is temporary or timed out, the corresponding If statement is executed and can be handled appropriately.

A direct comparison

A third way to get more detailed information about errors is to compare them directly with variables of the wrong type.

The Glob function of the Filepath package is used to return the names of all files that match the pattern. This function returns an error ErrBadPattern when a schema error occurs.

ErrBadPattern is defined in the Filepath package, as follows:

var ErrBadPattern = errors.New("syntax error in pattern")  
Copy the code

** errors.new ()** Used to create New errors.

When a pattern error occurs, ErrBadPattern is returned by the Glob function:

func main(a) {  
    files, error := filepath.Glob("[")
    iferror ! =nil && error == filepath.ErrBadPattern {
        fmt.Println(error)
        return
    }
    fmt.Println("matched files", files)
}
Copy the code

Running results:

syntax error in pattern  
Copy the code

Don’t ignore mistakes

Never ignore a mistake. To overlook mistakes is to invite trouble. The following example lists the names of all files that match the pattern, ignoring the error handling code.

func main(a) {  
    files, _ := filepath.Glob("[")
    fmt.Println("matched files", files)
}
Copy the code

Using a blank identifier in the line number ignores the error returned by the Glob function:

matched files []  
Copy the code

By ignoring this error, the output looks as if no files match the pattern, but the pattern itself is malformed. So don’t ignore errors.

Custom error

Custom errors can be created using the New() function under the Errors package and the: Errorf() function under the FMT package.

/ / errors package:
func New(text string) error {}

/ / FMT package:
func Errorf(format string, a ...interface{}) error {}
Copy the code

An implementation of the new functionality in the error package is provided below.

// Package errors implements functions to manipulate errors.
  package errors

  // New returns an error that formats as the given text.
  func New(text string) error {
      return &errorString{text}
  }

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

  func (e *errorString) Error(a) string {
      return e.s
  }
Copy the code

Now that you know how the New() function works, use it to create a custom error.

Create a simple program that calculates the area of a circle and returns an error if the radius is negative.

func circleArea(radius float64) (float64, error) {  
    if radius < 0 {
        return 0, errors.New("Area calculation failed, radius is less than zero")}return math.Pi * radius * radius, nil
}

func main(a) {  
    radius := 20.0
    area, err := circleArea(radius)
    iferr ! =nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("The Area of circle % 0.2 f", area)
}
Copy the code

Running results:

Area calculation failed, radius is less than zero 
Copy the code

Use Errorf to add more information to the error

The above program works fine, but it can be tricky to print out the actual radius that caused the error. This is where the Errorf function of the FMT package comes in. This function formats the error according to a format specifier and returns a string as a value to satisfy the error.

Use the Errorf function to modify the program:

func circleArea(radius float64) (float64, error) {  
    if radius < 0 {
        return 0, fmt.Errorf(Area Calculation failed, RADIUS %0.2f is less than zero", radius)
    }
    return math.Pi * radius * radius, nil
}

func main(a) {  
    radius := 20.0
    area, err := circleArea(radius)
    iferr ! =nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("The Area of circle % 0.2 f", area)
}
Copy the code

Running results:

Area calculation failed, radius-20.00 is less than zeroCopy the code

Use structures and fields to provide more information about errors

You can also use a struct type that implements an error interface as an error. This makes error handling more flexible. In the example above, if you want to access the radius that caused the error, the only way is to resolve that the error description area failed to be evaluated, and the radius -20.00 is less than zero. This is not the right approach, because if the description changes, the code breaks.

I mentioned earlier that asserting the underlying structure type gets more information from the struct field and uses the struct field to provide access to the radius that caused the error. You can create a struct type that implements the error interface and use its fields to provide more information about the error.

Create a struct type to indicate an error. The naming convention for Error types is that names should end with the text Error:

type areaError struct {  
    err    string
    radius float64
}
Copy the code

The struct type above has a field radius that stores the value of the radius responsible for the error, and the error field stores the actual error message.

2. Implement error interface

func (e *areaError) Error(a) string {  
    return fmt.Sprintf("The radius % 0.2 f: % s", e.radius, e.err)
}
Copy the code

In the code snippet above, a pointer sink area Error is used to implement the Error() string method of the Error interface. This method prints out the radius and error description.

type areaError struct {  
    err    string
    radius float64
}

func (e *areaError) Error(a) string {  
    return fmt.Sprintf("The radius % 0.2 f: % s", e.radius, e.err)
}

func circleArea(radius float64) (float64, error) {  
    if radius < 0 {
        return 0, &areaError{"radius is negative", radius}
    }
    return math.Pi * radius * radius, nil
}

func main(a) {  
    radius := 20.0
    area, err := circleArea(radius)
    iferr ! =nil {
        if err, ok := err.(*areaError); ok {
            fmt.Printf("Radius %0.2f is less than zero", err.radius)
            return
        }
        fmt.Println(err)
        return
    }
    fmt.Printf("The Area of circle % 0.2 f", area)
}
Copy the code

Program output:

Radius-20.00 is less than zeroCopy the code

Use the structure method to provide more information about the error

1. Create a structure to represent errors.

type areaError struct {  
    err    string //error description
    length float64 //length which caused the error
    width  float64 //width which caused the error
}
Copy the code

The error structure type above contains an error description field, along with the length and width that caused the error.

Implement the error interface and add methods to the error type to provide more information about the error.

func (e *areaError) Error(a) string {  
    return e.err
}

func (e *areaError) lengthNegative(a) bool {  
    return e.length < 0
}

func (e *areaError) widthNegative(a) bool {  
    return e.width < 0
}
Copy the code

In the code snippet above, the Error description of the Error() string method is returned. LengthNegative () bool returns true when length is less than 0; WidthNegative () bool returns true when the width is less than 0. These two methods provide more information about error.

Area calculation function:

func rectArea(length, width float64) (float64, error) {  
    err := ""
    if length < 0 {
        err += "length is less than zero"
    }
    if width < 0 {
        if err == "" {
            err = "width is less than zero"
        } else {
            err += ", width is less than zero"}}iferr ! ="" {
        return 0, &areaError{err, length, width}
    }
    return length * width, nil
}
Copy the code

The rectArea function above checks if the length or width is less than zero, and if it returns an error message, returns the area of the rectangle as nil.

Main function:

func main(a) {  
    length, width := 5.0.9.0
    area, err := rectArea(length, width)
    iferr ! =nil {
        if err, ok := err.(*areaError); ok {
            if err.lengthNegative() {
                fmt.Printf("error: length %0.2f is less than zero\n", err.length)

            }
            if err.widthNegative() {
                fmt.Printf("error: width %0.2f is less than zero\n", err.width)

            }
        }
        fmt.Println(err)
        return
    }
    fmt.Println("area of rect", area)
}
Copy the code

Running results:

Error: length-5.00 is less than zero Error: width -9.00 is less than zeroCopy the code

Wrong assertion

Once you have a custom error and carry more error information, you can use it. The returned error interface needs to be converted to a custom error type, called a type assertion.

Err.(*commonError) in the following code is the application of type assertions on the error interface, also known as error assertions.

sum, err := add(- 1.2)
ifcm,ok := err.(*commonError); ok{ fmt.Println("Error code :",cm.errorCode,", error message:",cm.errorMsg)
} else {
   fmt.Println(sum)
}
Copy the code

If ok is returned as true, the error assertion succeeds and a variable cm of type *commonError is correctly returned, so you can use the errorCode and errorMsg fields of the variable CM as in the example.

Nested error

Error Wrapping

The Error interface is compact, but also weak. Imagine the need to reproduce an error from an existing error. This is error nesting.

The need exists, for example, to call a function, return an error message, and then add some additional information to return a new error without wanting to lose the error message. The first thing that comes to mind is to customize a struct, as shown in the following code:

type MyError struct {
    err error
    msg string
}
Copy the code

This structure has two fields. The err field of error type is used to store the existing error, and the MSG field of string type is used to store the new error information, which is error nesting.

Now let the struct MyError implement the error interface, and pass the existing error and new error information when initializing MyError:

func (e *MyError) Error(a) string {
    return e.err.Error() + e.msg
}
func main(a) {
    // Err is an existing error that can be returned from another function
    newErr := MyError{err, "Data upload problem"}}Copy the code

This is fine, but cumbersome, because you have to define new types and implement the Error interface. So from Go 1.13, Error Wrapping function is added to Go standard library, which can generate new Error based on an existing Error and retain the original Error information:

e := errors.New("Original error E")
w := fmt.Errorf("Wrap error :%w", e)
fmt.Println(w)
Copy the code

Wrapping Error is generated by extending FMT.Errorf and adding a %w instead of Wrap.

Errors. Unwrap function

Since an error can be wrapped and nested to generate a new error, it can also be unwrapped, using the errors.Unwrap function to get nested errors.

The Go language provides errors.Unwrap to retrieve nested errors, such as the error variable w in the above example, which can be unwrapped to retrieve the nested original error E:

fmt.Println(errors.Unwrap(w))
Copy the code

You can see the message “original error E”.

Original error ECopy the code

Errors. The Is function

After Error Wrapping, it can be found that the method used to judge whether two errors are the same Error is invalid, for example, the method often used in the standard library of Go language is as follows:

if err == os.ErrExist
Copy the code

Why does this happen? Due to the Error Wrapping function of Go, it is not clear whether the returned ERR is nested and how many layers are nested.

So Go provides us with errors.Is, which checks whether two errors are the same:

func Is(err, target error) bool
Copy the code

Can be interpreted as:

Return true if err and Target are the same.

Returns true if err is a wrapping error and target is included in the nested error chain.

This can be summarized as simply returning true if two errors are equal or if err contains target, and false for the rest. Use the above example to determine whether error W contains error E:

fmt.Println(errors.Is(w,e))
Copy the code

Errors. As function

For the same reason, error assertions cannot be used with error nesting, because you do not know whether an error is nested and how many layers are nested. So Go provides the errors.As function to solve this problem. For example, the error assertion example above can be overridden using errors.As, which has the same effect:

var cm *commonError
if errors.As(err,&cm){
   fmt.Println("Error code :",cm.errorCode,", error message:",cm.errorMsg)
} else {
   fmt.Println(sum)
}
Copy the code

Therefore, under the Error Wrapping capability provided by Go language, Is and As functions should be used As much As possible for judgment and conversion.

Deferred function

In a custom function, you open a file and then need to close it to free resources. No matter how many branches the code executes or whether an error occurs, the file must be closed so that resources can be freed.

If this were done by a developer, it would become cumbersome with the complexity of the business logic, and you might forget to shut it down. In this case, the Go language provides the defer function, which guarantees that the file will be executed after closing, regardless of exceptions or errors in the custom function.

The following code is the ReadFile function from the Go standard package ioutil. It needs to open a file and then use the defer keyword to ensure that the f.close () method is executed at the end of the ReadFile function so that the file’s resources are definitely released.

func ReadFile(filename string) ([]byte, error) {
   f, err := os.Open(filename)
   iferr ! =nil {
      return nil, err
   }
   defer f.Close()
   // omit extraneous code
   return readAll(f, n)
}
Copy the code

The defer keyword is used to modify a function or method so that it will not execute until it returns, which is delayed but guaranteed to execute.

As an example of the ReadFile function above, the f.close method modified by defer executes late, meaning that readAll(f, n) is executed first, and then f.close is executed before the entire ReadFile function returns.

The defer statement is often used for paired operations, such as opening and closing files, locking and releasing locks, establishing and breaking connections, and so on. No matter how complex the operation, you can ensure that the resource is released correctly.

Panic () and recover ()

Golang introduces two built-in functions panic and Recover to trigger and terminate the exception handling process, while the keyword defer is introduced to delay the execution of the functions that follow defer. The deferred function (the function after defer) will not be executed until the function containing the defer statement completes execution, regardless of whether the function containing the defer statement ends normally by return or abnormally due to panic. You can execute multiple defer statements in a function in the reverse order of declaration. When the program is running, if a null pointer is referenced, subscript is out of bounds, or panic function is explicitly called, the execution of panic function is triggered first, and then the delay function is called. The caller continues to pass panic, so the process repeats in the call stack all the time: the function stops executing, the call delays executing the function, and so on. If the recover function is not called in the delay function along the way, the start of the coroutine is reached, the coroutine terminates, and all other coroutines are terminated, including the main coroutine (similar to the main coroutine in C, which has ID 1).

Panic:

  1. Built-in functions
  2. If F contains a panic statement, the code to be executed will be terminated. If F contains a list of defer functions to be executed, execute in reverse order of defer
  3. Return G, the caller of function F, where the code does not execute after the F statement is called. If there is a list of defer functions to execute in G, defer is executed in reverse order, which is somewhat similar to finally in try-catch-finally
  4. Until Goroutine exits altogether and reports an error

Recover:

  1. Built-in functions
  2. Used to control the panic behavior of a Goroutine, catching panic, and affecting the behavior of the application
  3. The general call advice is a). In the defer function, recever terminates a Goroutine panic procedure to resume normal code execution b). You can get errors passed through Panic

To put it simply: Go can throw an exception that causes panic, and then defer can catch it through Recover and process as normal.

Errors and exceptions are the difference between error and panic, in terms of the Golang mechanism. Many other languages do the same, such as C++/Java, which does not have error but has errno, and does not have panic but has throw.

Golang errors and exceptions are interchangeable:

  1. Error to exception, for example, the program logically tries to request a URL for a maximum of three times. If the request fails after three attempts, it is an error. If the request fails after the third attempt, the failure is promoted to an exception.
  2. For example, after an exception triggered by panic is recovered, variables of the error type in the returned value are assigned so that the upper-level function can continue the error processing process.

You have to have a set of rules for when to use errors and when to use exceptions, otherwise it’s easy to get everything wrong or everything out of order.

The scope (scenario) for exception handling is given below:

  1. Null pointer reference
  2. The subscript crossing the line
  3. Divisor of 0
  4. Branches that should not appear, such as default
  5. Input should not cause function errors

Other scenarios use error handling, which makes the function interface very refined. For exceptions, you can choose to recover at an appropriate upstream and print the stack information so that the deployed program does not terminate.

Note: Golang error handling has been criticized by many people, with some joking that half of the code is “if err! = nil {/ print && error handling /}”, seriously affecting normal processing logic. When we distinguish errors from exceptions and design functions according to rules, we greatly improve readability and maintainability.

Correct posture for error handling

Pose 1: Do not use error when there is only one cause of failure

func (self *AgentContext) CheckHostType(host_type string) error {
    switch host_type {
    case "virtual_machine":
        return nil
    case "bare_metal":
        return nil
    }
    return errors.New("CheckHostType ERROR:" + host_type)
}
Copy the code

This function can only fail for one reason, so the return type should be bool instead of error. Refactor this code:

func (self *AgentContext) IsValidHostType(hostType string) bool {
    return hostType == "virtual_machine" || hostType == "bare_metal"
}
Copy the code

Note: In most cases, there is more than one cause of failure. Especially for I/O operations, users need to know more error information. In this case, the return value type is error rather than simply bool

Gesture 2: Do not use error when there is no failure

Error is so popular in Golang that many people use error whenever they design functions, even if there is no single reason for failure:

func (self *CniParam) setTenantId(a) error {
    self.TenantId = self.PodNs
    return nil
}
Copy the code

For the above function design, there would be the following calling code:

err := self.setTenantId()
iferr ! =nil {
    // log
    // free resource
    return errors.New(...)
}
Copy the code

Refactor the code:

func (self *CniParam) setTenantId(a) {
    self.TenantId = self.PodNs
}
Copy the code

The calling code becomes:

self.setTenantId()
Copy the code

Position 3: Error should be placed at the end of the list of return value types

For the return value type error, which is used to convey an error message, it is usually placed last in Golang.

resp, err := http.Get(url)
iferr ! =nil {
    return nill, err
}
Copy the code

The same is true for bool as the return value type.

value, ok := cache.Lookup(key) 
if! ok {/ /... [key] the cache does not exist...
}
Copy the code

Position 4: Define error values consistently, rather than following your gut

When writing code, many people return errors.New(value) everywhere, and the error value may have different forms when expressing the same meaning. For example, the error value of “record does not exist” may be:

  1. “record is not existed.”
  2. “record is not exist!”
  3. “Record is not existed!!”
  4. .

As a result, the same error value is scattered over a large area of code. When the upper function wants to uniformly handle a particular error value, it needs to roam all the lower code to ensure the error value is unified. Unfortunately, sometimes some of the errors will be missed, and this way seriously hinders the reconstruction of the error value.

Therefore, we can refer to the C/C++ error code definition file and add an error object definition file to each Golang package, as follows:

var ERR_EOF = errors.New("EOF")
var ERR_CLOSED_PIPE = errors.New("io: read/write on closed pipe")
var ERR_NO_PROGRESS = errors.New("multiple Read calls return no data or error")
var ERR_SHORT_BUFFER = errors.New("short buffer")
var ERR_SHORT_WRITE = errors.New("short write")
var ERR_UNEXPECTED_EOF = errors.New("unexpected EOF")
Copy the code

Position 5: When errors are passed layer by layer, log each layer

Adding logs to each layer facilitates fault location.

Note: As far as testing to find faults, rather than logging, is concerned, many teams currently struggle with this. If you or your team can do it, ignore the pose.

Pose 6: Use defer for error handling

If the current operation fails, destroy the resource that has been created in this function. The following code is used as an example:

func deferDemo(a) error {
    err := createResource1()
    iferr ! =nil {
        return ERR_CREATE_RESOURCE1_FAILED
    }
    err = createResource2()
    iferr ! =nil {
        destroyResource1()
        return ERR_CREATE_RESOURCE2_FAILED
    }

    err = createResource3()
    iferr ! =nil {
        destroyResource1()
        destroyResource2()
        return ERR_CREATE_RESOURCE3_FAILED
    }

    err = createResource4()
    iferr ! =nil {
        destroyResource1()
        destroyResource2()
        destroyResource3()
        return ERR_CREATE_RESOURCE4_FAILED
    } 
    return nil
}
Copy the code

When Golang’s code executes, if a closure call from Defer is encountered, it is pushed onto the stack. When the function returns, closures are called in last-in, first-out order. Parameters to closures are passed by value, but external variables are passed by reference, so the value of the external variable err in the closure becomes the latest ERR value when the external function returns. Based on this conclusion, refactor the sample code above:

func deferDemo() error { err := createResource1() if err ! = nil { return ERR_CREATE_RESOURCE1_FAILED } defer func() { if err ! = nil { destroyResource1() } }() err = createResource2() if err ! = nil { return ERR_CREATE_RESOURCE2_FAILED } defer func() { if err ! = nil { destroyResource2() } }() err = createResource3() if err ! = nil { return ERR_CREATE_RESOURCE3_FAILED } defer func() { if err ! = nil { destroyResource3() } }() err = createResource4() if err ! = nil { return ERR_CREATE_RESOURCE4_FAILED } return nil }Copy the code

Position 7: Do not immediately return an error when you can avoid failure by trying several times

If the error is accidental, or caused by an unpredictable problem. A wise choice is to re-try an operation that failed, sometimes succeeding on the second or third try. When retrying, we need to limit the retry interval or the number of retries to prevent unlimited retries.

Two cases:

  1. When we go online, we try to request a URL, and sometimes the first time we don’t get a response, and then when we refresh, we get a surprise.
  2. One QA member of the team once suggested that when Neutron attach operation failed, it would be better to try three times, which proved to be effective in the current environment.

Position 8: It is recommended not to return error when the upper function does not care about errors

For some resource cleanup related functions (destroy/delete/clear), if a child function fails, it simply prints a log without reporting the error further to the upper function, which generally does not care about the result of execution or cannot do anything if it does. Therefore, we recommend that the relevant function be designed to not return error.

Gesture 9: Do not ignore useful return values when an error occurs

In general, when a function returns a non-nil error, the other return values are undefined, and these undefined return values should be ignored. However, there are a few functions that still return some useful value when an error occurs. For example, when an error occurs reading a file, the Read function returns the number of bytes that can be Read along with an error message. In this case, the read string should be printed along with the error message.

Description: The return value of a function should be clearly stated so that others can use it.

Correct posture for exception handling

Position 1: During the development phase, stick to the wrong speed

Speed wrong, simply speaking is “let it hang”, only hang you will be the first time to know the error. In early development and before any release phase, the simplest and probably best approach is to call panic to interrupt the program to force an error so that it is not ignored and can be fixed as quickly as possible.

Position 2: After the program is deployed, the exception should be recovered to avoid program termination

In Golang, if a Goroutine panic occurs and does not recover, the entire Golang process will exit unexpectedly. Therefore, once the Golang program is deployed, an exception should not cause the program to exit abnormally under any circumstances. To achieve this, we add a delayed recover call to the upper function, and whether or not to recover depends on the environment variable or configuration file. Recover is required by default. This posture is similar to assertions in C, but there is a difference: generally in the Release, assertions are defined as null and invalid, but an if checkpresence is required for exception protection, although this is not recommended in design by contract. In Golang, Recover completely stops the abnormal expansion process, saving time and effort.

We respond to this exception in the most reasonable way in the delay function that calls RECOVER:

  1. Print stack exception call information and critical business information so that these problems remain visible;
  2. The exception is turned into an error so that the caller can restore the program to a healthy state and continue to run safely.

A simple example:

func funcA(a) error {
    defer func(a) {
        if p := recover(a); p ! =nil {
            fmt.Printf("panic recover! p: %v", p)
            debug.PrintStack()
        }
    }()
    return funcB()
}

func funcB(a) error {
    // simulation
    panic("foo")
    return errors.New("success")}func test(a) {
    err := funcA()
    if err == nil {
        fmt.Printf("err is nil\\n")}else {
        fmt.Printf("err is %v\\n", err)
    }
}
Copy the code

We expect the output of test to be:

err is foo
Copy the code

The test function actually outputs:

err is nil
Copy the code

The reason is that the panic exception handling mechanism does not automatically pass error information to error, so do this explicitly in the funcA function, as follows:

func funcA(a) (err error) {
    defer func(a) {
        if p := recover(a); p ! =nil {
            fmt.Println("panic recover! p:", p)
            str, ok := p.(string)
            if ok {
                err = errors.New(str)
            } else {
                err = errors.New("panic")
            }
            debug.PrintStack()
        }
    }()
    return funcB()
}
Copy the code

Position 3: Use exception handling for branches that should not occur

When something that shouldn’t happen happens, we should call panic to trigger the exception. For example, when the program reaches some logically impossible path:

switch s := suit(drawCard()); s {
    case "Spades":
    // ...
    case "Hearts":
    // ...
    case "Diamonds":
    // ... 
    case "Clubs":
    // ...
    default:
        panic(fmt.Sprintf("invalid suit %v", s))
}
Copy the code

Position 4: Use Panic design for functions whose input arguments should not have problems

There should be no problem with the input parameter. This usually means hard coding, but let’s look at the two functions (Compile and MustCompile), where the MustCompile function is a wrapper around the Compile function:

func MustCompile(str string) *Regexp {
    regexp, error := Compile(str)
    iferror ! =nil {
        panic(`regexp: Compile(` + quote(str) + `) : ` + error.Error())
    }
    return regexp
}
Copy the code

So, in cases where both the user input scenario and the hard-coded scenario are supported, the functions that support the hard-coded scenario are generally wrapped around the functions that support the user input scenario. In cases where only a single hard-coded scenario is supported, the function is designed to use panic directly, meaning that there is no error in the list of return value types, which makes the handling of function calls very convenient (without the tedious “if err! = nil {/ prints && error handling /}” code block).

The practice of false encapsulation

User-defined type

The built-in error type in the rewrite of Go starts with a custom error type that will be identified as error in the program. Therefore, a new custom error type is introduced that encapsulates Go’s error.

type GoError struct {
   error
}
Copy the code

Context data

When error is said to be a value in Go, it is a string value – any type that implements the error () string function can be considered an error type. Treating string values as error complicates error handling across layers, so handling error string information is not the right approach. So you can use nested errors to decouple the string from the error code:

type GoError struct {
   error
   Code    string
}
Copy the code

Error handling is now based on the error Code field instead of a string. Error strings can be further decoupled from context data, where they can be internationalized using the I18N package.

type GoError struct {
   error
   Code    string
   Data    map[string]interface{}}Copy the code

Data contains the context Data used to construct the error string. Error strings can be templated with data:

//i18N def
"InvalidParamValue": "Invalid parameter value '{{.actual}}', expected '{{.expected}}' for '{{.name}}'"
Copy the code

In the i18N definition file, the error Code will be mapped to the templated error string built using Data.

The reasons (Causes)

An error can occur at any level, and it is necessary to provide options for each level to handle the error and to further wrap the error with additional context information without losing the original error value. The GoError structure can be further encapsulated with Causes, which holds the entire error stack.

type GoError struct {
   error
   Code    string
   Data    map[string]interface{}
   Causes  []error
}
Copy the code

If you must save more than one error data, causes is an array type and is set to the basic error type to include third-party errors for that cause in your program.

Components (Component)

The tag layer component will help identify the layer at which the error occurred and can avoid unnecessary error wrap. For example, if an Error component of type Service occurs at the service layer, a Wrap Error may not be required. Checking component information will help prevent exposure to errors that the user should not be notified of, such as database errors:

type GoError struct {
   error
   Code      string
   Data      map[string]interface{}
   Causes    []error
   Component ErrComponent
}

type ErrComponent string
const (
   ErrService  ErrComponent = "service"
   ErrRepo     ErrComponent = "repository"
   ErrLib      ErrComponent = "library"
)
Copy the code

ResponseType

Add an error response type so that you can support the error classification so you can understand what error type is. For example, errors can be classified by response type (such as NotFound), and errors like DbRecordNotFound, ResourceNotFound, UserNotFound, and so on can be classified as NotFound errors. This is useful in multi-tier application development and is optional encapsulation:

type GoError struct {
   error
   Code         string
   Data         map[string]interface{}
   Causes       []error
   Component    ErrComponent
   ResponseType ResponseErrType
}

type ResponseErrType string

const (
   BadRequest    ResponseErrType = "BadRequest"
   Forbidden     ResponseErrType = "Forbidden"
   NotFound      ResponseErrType = "NotFound"
   AlreadyExists ResponseErrType = "AlreadyExists"
)
Copy the code

retry

In rare cases, an error is retried. The Retry field determines whether error retries are required by setting the Retryable flag:

type GoError struct {
   error
   Code         string
   Message      string
   Data         map[string]interface{}
   Causes       []error
   Component    ErrComponent
   ResponseType ResponseErrType
   Retryable    bool
}
Copy the code

GoError interface

Error checking can be simplified by defining an explicit error interface with a GoError implementation:

package goerr

type Error interface {
   error

   Code() string
   Message() string
   Cause() error
   Causes() []error
   Data() map[string]interface{}
   String() string
   ResponseErrType() ResponseErrType
   SetResponseType(r ResponseErrType) Error
   Component() ErrComponent
   SetComponent(c ErrComponent) Error
   Retryable() bool
   SetRetryable() Error
}
Copy the code

Abstract the error

With the encapsulation described above, it is even more important to abstract error, keep these wrappers in the same place, and provide the reusability of error functions

func ResourceNotFound(id, kind string, cause error) GoError {
   data := map[string]interface{} {"kind": kind, "id": id}
   return GoError{
      Code:         "ResourceNotFound",
      Data:         data,
      Causes:       []error{cause},
      Component:    ErrService,
      ResponseType: NotFound,
      Retryable:    false,}}Copy the code

This error function abstractions ResourceNotFound. Developers can use this function to return error objects instead of creating new objects each time:

//UserService
user, err := u.repo.FindUser(ctx, userId)
iferr ! =nil {
   if err.ResponseType == NotFound {
      return ResourceNotFound(userUid, "User", err)
   }
   return err
}
Copy the code

conclusion

We demonstrated how to make error more meaningful in a multi-tier application by using a custom Go error type that adds context data. You can see the full code implementation and definition here [1].

The resources

[1] here: gist.github.com/prathabk/74…