SoFunction
Updated on 2025-03-05

Detailed explanation of error handling examples in Golang

1、panic

When we execute panic, the following process will be ended:

package main

import "fmt"

func main() {
	("hello")
	panic("stop")
	("world")
}

Output:

go run  
hello
panic: stop

But panic can also be captured, we can use defer and recover to implement it:

package main

import "fmt"

func main() {

	defer func() {
		if r := recover(); r != nil {
			("recover: ", r)
		}
	}()

	("hello")
	panic("stop")
	("world")
}

Output:

go run
hello
recover:  stop

Then when will panic be suitable? In Go, panic is used to represent real exceptions, such as program errors. We often see panic in some built-in packages.

For example, repeatedly return a new string composed of a counted copy of the string s:

func Repeat(s string, count int) string {
	if count == 0 {
		return ""
	}

	// 
	if count < 0 {
		panic("strings: negative Repeat count")
	} else if len(s)*count/count != len(s) {
		panic("strings: Repeat count causes overflow")
	}

	...
}

We can see that when the number of repetitions is less than 0 or the length of s overflows after counts, the program will directly panic instead of returning an error. At this time, because the strings package restricts the use of errors, it will be directly paniced when the program is incorrect.

Another example is about regular expressions:

package main

import (
	"fmt"
	"regexp"
)

func main() {
	pattern := "a[a-z]b*" // 1
	compile, err := (pattern) // 2
	if err != nil { // 2
		("compile err: ", err)
		return
	}
  // 3
	allString := ("acbcdadb", 3)
	(allString)

}
  • Write a regular expression
  • Call Compile, parsing the regular expression, and if successful, return the Regexp object used to match the text. Otherwise, an error will be returned
  • Using the regularity, get all matching characters in the input string

You can see that if the above regular parsing fails, you can continue to execute it, but there is another method in the regexp package MustCompile:

func MustCompile(str string) *Regexp {
	regexp, err := Compile(str)
	if err != nil {
		panic(`regexp: Compile(` + quote(str) + `): ` + ())
	}
	return regexp
}

This method shows that regular parsing is strongly dependent. If parsing is wrong, panic will directly end the program. Users can choose according to actual situations.

However, in actual development, we should still use panic with caution, because it will make the program run (unless we call defer recover)

2. Packaging error

Error packaging is to package or package the error in a packaging container, so that we can trace the source error. The main function of incorrect packaging is:

  • Add context to error
  • Mark an error as a specific type of error

We can see an example of accessing a database:

package main

import (
	"fmt"
	"/pkg/errors"
)

type Courseware struct {
	Id int64
	Code string
	Name string
}

func getCourseware(id int64) (*Courseware, error) {
	courseware, err := getFromDB(id)
	if err != nil {
		return nil, (err, "I want to access this courseware in June") // 2
	}
	return courseware, nil
}

func getFromDB(id int64) (*Courseware, error) {
	return nil, ("permission denied") // 1
}

func main() {
	_, err := getCourseware(11)
	if err != nil {
		(err)
	}
}
  • When accessing the database, we returned the original error message
  • To the upper layer we added some custom context information

Output:

go run
Want to access this courseware in June: permission denied

Of course, we can also wrap the errors into our custom type errors. Let’s slightly modify the above example:

package main

import (
	"fmt"
	"/pkg/errors"
)

type Courseware struct {
	Id int64
	Code string
	Name string
}

// 1
type ForbiddenError struct {
	Err error
}

// 2
func (e *ForbiddenError) Error() string {
	return "Forbidden: " + ()
}

func getCourseware(id int64) (*Courseware, error) {
	courseware, err := getFromDB(id)
	if err != nil {
		return nil, &ForbiddenError{err} // 4
	}
	return courseware, nil
}

func getFromDB(id int64) (*Courseware, error) {
	return nil, ("permission denied") // 3
}

func main() {
	_, err := getCourseware(11)
	if err != nil {
		(err)
	}
}
  • First we customized the error type of ForbiddenError
  • We implemented the error interface
  • Accessing the database throws original error
  • The upper layer returns the error of ForbiddenError type

Output:

go run
Forbidden: permission denied

Of course, we can also add context without creating a custom error type:

package main

import (
	"fmt"
	"/pkg/errors"
)

type Courseware struct {
	Id int64
	Code string
	Name string
}


func getCourseware(id int64) (*Courseware, error) {
	courseware, err := getFromDB(id)
	if err != nil {
		return nil, ("another wrap err: %w", err) // 1
	}
	return courseware, nil
}

func getFromDB(id int64) (*Courseware, error) {
	return nil, ("permission denied")
}

func main() {
	_, err := getCourseware(11)
	if err != nil {
		(err)
	}
}

Error wrapping with %w

The advantage of using this is that we can trace the source errors, which facilitates us to do some special handling.

Another way is to use:

return nil, ("another wrap err: %v", err)

The %v method does not wrap the error, so the source error cannot be traced, but often sometimes we choose this method instead of using the %w method. Although the %w method can wrap the source error, we often deal with it through the source error. If the source error is modified, then the relevant errors that wrap the source error need to be changed in response.

3. Error type judgment

Let's expand the above example of querying courseware. Now we have this judgment. If the id is passed in is illegal, we return 400 errors, and if the database query errors are reported, we return 500 errors, we can write like below:

package main

import (
	"fmt"
	"/pkg/errors"
)

type Courseware struct {
	Id int64
	Code string
	Name string
}

type ForbiddenError struct {
	Err error
}

func (e *ForbiddenError) Error() string {
	return "Forbidden: " + ()
}

func getCourseware(id int64) (*Courseware, error) {
	if id &lt;= 0 {
		return nil, ("invalid id: %d", id)
	}
	courseware, err := getFromDB(id)
	if err != nil {
		return nil, &amp;ForbiddenError{err}
	}
	return courseware, nil
}

func getFromDB(id int64) (*Courseware, error) {
	return nil, ("permission denied")
}

func main() {
	_, err := getCourseware(500) // We can modify the id here to see the printed structure	if err != nil {
		switch err := err.(type) {
		case *ForbiddenError:
			("500 err: ", err)
		default:
			("400 err: ", err)
		}
	}
}

Output:

go run
500 err:  Forbidden: permission denied

It seems that there is no problem with this. Now let’s modify the code a little and wrap the ForbiddenError above:

package main

import (
	"fmt"
	"/pkg/errors"
)

type Courseware struct {
	Id int64
	Code string
	Name string
}

type ForbiddenError struct {
	Err error
}

func (e *ForbiddenError) Error() string {
	return "Forbidden: " + ()
}

func getCourseware(id int64) (*Courseware, error) {
	if id &lt;= 0 {
		return nil, ("invalid id: %d", id)
	}
	courseware, err := getFromDB(id)
	if err != nil {
		return nil, ("wrap err: %w", &amp;ForbiddenError{err}) // A layer of error is wrapped here	}
	return courseware, nil
}

func getFromDB(id int64) (*Courseware, error) {
	return nil, ("permission denied")
}

func main() {
	_, err := getCourseware(500)
	if err != nil {
		switch err := err.(type) {
		case *ForbiddenError:
			("500 err: ", err)
		default:
			("400 err: ", err)
		}
	}
}

Output:

go run
400 err:  wrap err: Forbidden: permission denied

You can see that our Forbidden error entered 400, which is not the result we want. The reason for this is that there is another layer of Error error wrapped outside the ForbiddenError. When using type assertion, it is determined that the Error error is found, so it enters the 400 branch.

Here we can use the method, which will recursively call the Unwrap method and find the first method in the error chain that matches the target:

package main

import (
	"fmt"
	"/pkg/errors"
)

type Courseware struct {
	Id int64
	Code string
	Name string
}

type ForbiddenError struct {
	Err error
}

func (e *ForbiddenError) Error() string {
	return "Forbidden: " + ()
}

func getCourseware(id int64) (*Courseware, error) {
	if id &lt;= 0 {
		return nil, ("invalid id: %d", id)
	}
	courseware, err := getFromDB(id)
	if err != nil {
		return nil, ("wrap err: %w", &amp;ForbiddenError{err})
	}
	return courseware, nil
}

func getFromDB(id int64) (*Courseware, error) {
	return nil, ("permission denied")
}

func main() {
	_, err := getCourseware(500)
	if err != nil {
		var f *ForbiddenError // The *ForbiddenError interface is implemented here, otherwise it will be panic		if (err, &amp;f) { // Error finding a match			("500 err: ", err)
		} else {
			("400 err: ", err)
		}
	}
}

Output:

go run
500 err:  wrap err: Forbidden: permission denied

4. Error value judgment

We often see such global errors in the code, in the mysql library or the io library:

var ErrCourseware = ("courseware")

We call this error sentinel error. Generally, the database does not find ErrNoRows or io reads EOF errors. These specific errors can help us do some special processing.

Generally, we will use the == sign to judge the error value directly, but like the above, if the error is wrapped, it will be difficult for us to judge. Fortunately, the errors package provides a method to determine whether the error value in the error chain matches the target error by calling Unwrap recursively:

if err != nil {
    if (err, ErrCourseware) {
        // ...
    } else {
        // ...
    }
}

This is the end of this article about detailed explanation of error handling in Golang. For more related Golang error handling content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!