go error handling

I believe many people have experienced the error design of Go language (golang). It forces the caller to handle the error by returning a value. Either you ignore it or you handle it (processing can also continue to return it to the caller). For the design of golang, we will write a lot of if judgment in the code to make a decision.

func main() {
	conent,err:=ioutil.ReadFile("filepath")
	if err !=nil{
		//error handling
	}else {
		fmt.Println(string(conent))
	}
}

This kind of code is very important in our coding. In most cases, the error is nil, that is, there is no error, but when it is not nil, it means that the error occurs, and we need to deal with it.

error interface

error is actually an interface, built-in. Let's see its definition

// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
	Error() string
}
type fileError struct {
}

func (fe *fileError) Error() string {
	return "file error"
}

Custom error

A fileError type is customized and the error interface is implemented. Now test to see the effect.

func main() {
	conent, err := openFile()
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println(string(conent))
	}
}

//Just simulate an error
func openFile() ([]byte, error) {
	return nil, &fileError{}
}

When we run the simulated code, we can see the notification of file error.

In the actual use process, we may encounter many errors. The difference is that the Error information is different. One way is to define an Error type similar to the above, but this is too troublesome. We found that the Error returned is actually a string. We can modify it so that the string can be set.

type fileError struct {
	s string
}

func (fe *fileError) Error() string {
	return fe.s
}

Well, after this transformation, we can set the error text to prompt when declaring fileError, which can meet our different needs.

//Just simulate an error
func openFile() ([]byte, error) {
	return nil, &fileError{"File error, custom"}
}

Well, yes, we have achieved our goal. Now we can make it more general, such as changing the name of fileError and creating an auxiliary function, so that we can create different error types.

//blog:www.flysnow.org
//wechat:flysnow_org
func New(text string) error {
	return &errorString{text}
}

type errorString struct {
	s string
}

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

In this way, we can use the New function to help us create different errors. In fact, this is the error.New function we often use. It has been analyzed and evolved step by step. Now we have a clear understanding of the built-in error of Go language (golang).

Existing problems

Although the Go language is very concise for error design, it is obviously insufficient for our developers. For example, we need to know more information about the error, what file and which line of code? Only in this way can we locate the problem more easily.

For example, we want to attach more information to the returned error before returning. For example, in the above example, what should we do? We can only take out the original error information through the error method, then splice it ourselves, and then use the errors.New function to generate a new error return.

If we have done java development before, we know that Java exceptions can be nested, that is, through this, we can easily know the root cause of the error, because Java exceptions are nested and returned layer by layer. No matter how many packages are experienced in the middle, we can find the root cause of the error through cause.

solve the problem

If we want to solve the above problems, we must first continue to expand our errorString and add some fields to store more information. For example, we need to record stack information.

type stack []uintptr
type errorString struct {
	s string
	*stack
}

Welcome to WeChat official account flysnow_org or blog site   Feixue's ruthless blog focuses on Android, Java, Go language (golang), mobile Internet, project management and software architecture   See more original articles.

With the stack field storing stack information, we can store the stack information of the call in this field when generating an error.

//blog:www.flysnow.org
//wechat:flysnow_org

func callers() *stack {
	const depth = 32
	var pcs [depth]uintptr
	n := runtime.Callers(3, pcs[:])
	var st stack = pcs[0:n]
	return &st
}

func New(text string) error {
	return &errorString{
		s:   text,
		stack: callers(),
	}
}

Perfect solution. Now what if we solve the problem of adding some information to the existing errors? I believe you should have ideas.

type withMessage struct {
	cause error
	msg   string
}

func WithMessage(err error, message string) error {
	if err == nil {
		return nil
	}
	return &withMessage{
		cause: err,
		msg:   message,
	}
}

Use the WithMessage function to wrap the original error to generate a new error with wrapped information.

Recommended scheme

The above problem we are solving is, are we familiar with the methods we take? Especially look at the source code. Yes, this is the source code of the error handling library github.com/pkg/errors.

Because the errors provided by Go language are so simple that we can't handle problems better, or even provide us with more useful information. Therefore, many error handling libraries have been born. github.com/pkg/errors is relatively simple and powerful, which is welcomed by a large number of developers and many users.

Its use is very simple. If we want to generate a New error, we can use the New function to generate the error with its own call stack information.

func New(message string) error

If there is a ready-made error, we need to wrap it again. At this time, there are three functions to choose from.

//Attach only new information
func WithMessage(err error, message string) error

//Attach only call stack information
func WithStack(err error) error

//Attach stack and information at the same time
func Wrap(err error, message string) error

In fact, the above packaging is very similar to the exception packaging of Java. The packaged error is actually Cause. The root Cause of the error mentioned in the previous chapter is this Cause. Therefore, the error handling library provides us with the Cause function so that we can get the most fundamental error Cause.

Use the for loop to find the most fundamental (lowest) error.

We have packaged and collected the above errors, so how to print the stack, error reason and other information stored in them? In fact, the error types of this error handling library all implement the Formatter interface. We can output the corresponding error information through the fmt.Printf function.

%s,%v //The function is the same. It outputs error information without stack
%q //The output error message is quoted and does not contain the stack
%+v //Output error message and stack

If there are circular packing error types above, these errors will be output recursively.

Summary

By using this   github.com/pkg/errors   Error library, we can collect more information and make it easier for us to locate problems.

The information we collected can not only be output to the console, but also be used as a Log, which can be output to the corresponding Log log to facilitate problem analysis.

It is said that this library will be added to the Golang standard SDK. I look forward to adding the errors in the current standard library   This package.

Tags: Go Error

Posted on Mon, 22 Nov 2021 18:37:05 -0500 by PickledOnion