07 Error Handling How to Handle Errors With Error, Defer, Panic, Etc

07 Error Handling - How to Handle Errors with error, defer, panic, etc #

In the last lesson, I explained structs and interfaces to you, and left a small homework for you to practice implementing an interface with two methods. Now, I will use the example of “a person can walk and run” to explain.

First, define an interface WalkRun with two methods Walk and Run, as shown in the code below:

type WalkRun interface {
   Walk()
   Run()
}

Now, you can make the person struct implement this interface, as shown below:

func (p *person) Walk(){
   fmt.Printf("%s can walk\n",p.name)
}

func (p *person) Run(){
   fmt.Printf("%s can run\n",p.name)
}

The key point is to implement each method of the interface, which means implementing the interface.

Note: %s is a placeholder, which corresponds to p.name, i.e., the value of p.name. You can refer to the documentation of the fmt.Printf function for more details.

Next, let’s move on to this lesson. In this lesson, I will teach you about error and exception handling in the Go language. When we write programs, we may encounter some problems, so how do we handle them?

Errors #

In Go, errors are expected and not very serious, and they do not affect the execution of the program. For this type of issue, you can use the method of returning errors to the caller, and let the caller decide how to handle them.

error interface #

In Go, errors are represented by the built-in error interface. It is very simple and only has one Error method to return the specific error message, as shown in the code below:

type error interface {
   Error() string
}

In the code below, I demonstrate an example of converting a string to an integer:

ch07/main.go

func main() {
   i,err:=strconv.Atoi("a")
   if err!=nil {
      fmt.Println(err)
   }else {
      fmt.Println(i)
   }
}

Here, I intentionally use the string “a” and try to convert it to an integer. We know that “a” cannot be converted to a number, so when running this program, the following error message will be printed:

strconv.Atoi: parsing "a": invalid syntax

This error message is returned through the error interface. Let’s see the definition of the strconv.Atoi function:

func Atoi(s string) (int, error)

In general, the error interface is used to return when a method or function encounters an error, and it is usually the second return value. In this way, the caller can decide how to proceed based on the error message.

Note: Because methods and functions are basically the same, the difference is only whether there is a receiver, so from now on when I mention methods or functions, I mean the same thing and won’t write both names.

error factory function #

In addition to using other functions, your own defined functions can also return error messages to the caller, as shown in the code below:

ch07/main.go

func add(a,b int) (int,error){
   if a<0 || b<0 {
      return 0,errors.New("a or b cannot be negative")
   }else {
      return a+b,nil
   }
}

The add function will return an error message if either a or b is a negative number. If both a and b are not negative, the error part will return nil, which is a common practice. Therefore, the caller can determine how to handle the error by checking if the error message is nil.

The following add function example uses the errors.New factory function to generate an error message. It takes a string parameter and returns an error interface. These were explained in detail in the previous lesson on structs and interfaces, so I won’t go into detail here.

ch07/main.go

sum,err:=add(-1,2)
if err!=nil {
   fmt.Println(err)
}else {
   fmt.Println(sum)
}

Custom errors #

You may wonder that the method of using a factory to return an error message can only pass a string, i.e., the information carried is only a string. What if you want to pass more information, such as error code information? This is where custom errors are needed.

To customize an error, you first need to define a new type, such as a struct, and then make this type implement the error interface, as shown in the code below:

ch07/main.go

type commonError struct {
   errorCode int // error code
   errorMsg string // error message
}

func (ce *commonError) Error() string{
   return ce.errorMsg
}

With the custom error, you can use it to carry more information. Now, I will modify the previous example to return the newly defined commonError, as shown below:

ch07/main.go

return 0, &commonError{
   errorCode: 1,
   errorMsg:  "a or b cannot be negative"}

I create a *commonError using a literal notation, where the value of errorCode is 1 and the value of errorMsg is “a or b cannot be negative”.

Error assertion #

With the custom error and more error information, you can now use this information. You need to first convert the returned error interface into the custom error type, using the knowledge of type assertion from the previous lesson.

The code err.(*commonError) in the following code is an application of type assertion on the error interface, which can also be called error assertion.

ch07/main.go

sum, err := add(-1, 2)
if cm,ok:=err.(*commonError);ok{
   fmt.Println("Error code:",cm.errorCode,", Error message:",cm.errorMsg)
} else {
   fmt.Println(sum)
}

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

Error nesting #

Error Wrapping #

Although the error interface is concise, its functionality is relatively weak. Imagine if we have a requirement to generate an error based on an existing error, how can we do that? This is called error nesting.

This requirement exists, for example, when calling a function that returns an error message. In this case, without losing the existing error, you might want to add some additional information and return a new error. Therefore, our first thought should be to define a struct, as shown in the code below:

type MyError struct {
    err error
    msg string
}

This struct has two fields, the err field of type error is used to store the existing error, and the msg field of type string is used to store the new error message. This is called error nesting.

Now, let the MyError struct implement the error interface, and pass the existing error and the new error message when initializing MyError, as shown in the code below:

func (e *MyError) Error() string {
    return e.err.Error() + e.msg
}

func main() {
    // err is an existing error that can be returned from another function
    newErr := MyError{err, "data upload problem"}
}

This approach can meet our needs, but it is cumbersome because we need to define a new type and implement the error interface. Therefore, starting from Go 1.13, the Go standard library introduced Error Wrapping functionality, which allows us to generate a new error based on an existing error and retain the original error information. The following code demonstrates this:

ch07/main.go

e := errors.New("original error e")
w := fmt.Errorf("wrapped an error: %w", e)
fmt.Println(w)

Instead of providing a Wrap function, Go extends the fmt.Errorf function and adds %w to generate a wrapping error.

errors.Unwrap Function #

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

Go provides errors.Unwrap to retrieve the nested error. For example, in the above example, we can unwrap the error variable w to obtain the original error e.

Let’s run the following code:

fmt.Println(errors.Unwrap(w))

We can see the message “original error e”.

errors.Is Function #

With Error Wrapping, the method we used to check if two errors are the same is no longer effective. For example, the following code is often used in the Go standard library:

if err == os.ErrExist

Why is this the case? It’s because Go’s Error Wrapping functionality hides the nesting of the returned error and we don’t know how many layers deep it is.

To address this, Go provides the errors.Is function to check if two errors are the same:

func Is(err, target error) bool

The function should be interpreted as:

  • If err and target are the same, return true.
  • If err is a wrapping error and target is included in the nested error chain of err, return true.

In other words, if two errors are equal or if err contains target, it returns true; otherwise, it returns false. We can use the previous example to check if error w contains error e. Try running the following code and see if it prints true:

fmt.Println(errors.Is(w, e))

errors.As Function #

For the same reason, with nested errors, error assertions are also not feasible because we don’t know if an error is nested and to what extent. Therefore, to solve this problem, Go provides the errors.As function. For example, the previous error assertion example can be rewritten using the errors.As function and achieve the same effect, as shown in the following code:

ch07/main.go

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

Therefore, when using the Error Wrapping capability provided by Go, it is recommended to use functions like Is and As for comparison and type assertion.

Deferred Functions #

In a custom function, you open a file and then need to close it to release resources. Regardless of how many branches your code executes or whether errors occur, the file must be closed in order to ensure that resources are released.

If this task is left to the developer, it can become very cumbersome as the business logic becomes more complex, and it is also possible to forget to close the file. In light of this situation, Go provides us with the defer function, which ensures that the file is closed after the ReadFile function is finished, regardless of whether the custom function encounters an exception or error.

The code below is the ReadFile function in the Go language’s standard package ioutil. It opens a file and ensures that the f.Close() method is executed after the ReadFile function finishes by using the defer keyword. This guarantees the release of the file’s resources.

func ReadFile(filename string) ([]byte, error) {
   f, err := os.Open(filename)
   if err != nil {
      return nil, err
   }
   defer f.Close()
   // Omitted irrelevant code
   return readAll(f, n)
}

The defer keyword is used to modify a function or method, making it execute just before the return and thus delaying its execution, but ensuring that it is executed again.

Using the ReadFile function as an example, the f.Close method delayed by defer will be executed after readAll(f, n) is executed, but before the entire ReadFile function returns.

The defer statement is often used for paired operations, such as opening and closing files, acquiring and releasing locks, establishing and disconnecting connections, and so on. Regardless of how complex the operation is, it ensures that resources are released correctly.

Panic Exceptions #

Go is a statically typed, strongly typed language that captures many problems at compile time, but it can only detect some at runtime. Examples include array index out of bounds access and forced type conversion of different types, which can cause panic exceptions.

In addition to runtime-generated panics, we can also throw panic exceptions ourselves. For example, let’s say I need to connect to a MySQL database, I can write a function connectMySQL to connect to MySQL as shown in the code below:

func connectMySQL(ip,username,password string){
   if ip =="" {
      panic("ip不能为空")
   }
   // Omitted other code
}

In the connectMySQL function, if the ip is empty, it will directly throw a panic exception. This logic is correct because if the database cannot be successfully connected, running the whole program is meaningless, so we throw a panic to terminate the program.

Panic is a built-in function in Go that can accept a parameter of the interface{} type, which means it can accept a value of any type. The function definition is as follows:

func panic(v interface{})

Note: interface{} represents an empty interface, meaning it can represent values of any type in Go.

Panic exceptions are a very serious situation that can cause the program to crash and interrupt execution, so if it is an error that does not affect program execution, do not use panic; use ordinary error handling through error instead.

pDE7ppQNyfRSIn1Q__thumbnail.png

Recovering Panic Exceptions with Recover #

In general, we do not do anything with panic exceptions because they are exceptions that affect program execution, so we just let them crash directly. However, there are some special cases where we need to recover from panic exceptions in order to perform some resource release handling before the program crashes.

In Go, panic exceptions can be recovered using the built-in recover function. Since only functions decorated with defer will be executed when a program crashes due to a panic exception, the recover function must be used in conjunction with the defer keyword to take effect.

The example below shows how to recover from a panic exception using the combination of defer, anonymous functions, and the recover function.

func main() {
   defer func() {
      if p:=recover();p!=nil{
         fmt.Println(p)
      }
   }()
   connectMySQL("","root","123456")
}

Running this code will output the following, which demonstrates that the recover function successfully catches the panic exception.

ip 不能为空

From the output, it can be observed that the value returned by the recover function is the parameter passed to the panic function.

Summary #

This lesson mainly covers Go’s error handling mechanisms, including error, defer, panic, etc. In the two error handling mechanisms—error and panic—Go encourages the use of lightweight errors (error) instead of panic.