Preface
Go's error handling is a common problem for everyone in daily life. I have also observed some phenomena in my work. What is more serious is that the error handling is somewhat repetitive in the logical code at all levels.
For example, when someone writes code, he will judge errors and record logs at each layer. From the code level, it seems very rigorous, but if you look at the log, you will find a bunch of duplicate information, which will cause interference when troubleshooting the problem.
Today I will summarize three best practices related to Go code error handling for you.
These best practices were also shared by some seniors on the Internet. After practicing them myself, I described them in my own language here. I hope they will be helpful to everyone.
Meet error
Go program indicates error by error type value
The error type is a built-in interface type, which only specifies an Error method that returns the string value.
type error interface { Error() string }
Go language functions often return an error value, and the caller performs error processing by testing whether the error value is nil.
i, err := ("42") if err != nil { ("couldn't convert number: %v\n", err) return } ("Converted integer:", i)
When error is nil, it means success; if error is not nil, it means failure.
Remember to implement the error interface for custom error
We often define error types that meet our needs, but remember to enable these types to implement an error interface so that we do not need to introduce additional types into the caller's program.
For example, we have defined the type myError ourselves below. If the error interface is not implemented, the caller's code will be invaded by the type myError. For example, the following run function can be defined as an error when defining the return value type.
package myerror import ( "fmt" "time" ) type myError struct { Code int When What string } func (e *myError) Error() string { return ("at %v, %s, code %d", , , ) } func run() error { return &MyError{ 1002, (), "it didn't work", } } func TryIt() { if err := run(); err != nil { (err) } }
If myError does not implement the error interface, the return value type here must be defined as myError type. It can be imagined that the caller's program must use == xxx to determine which specific error it is (of course, if you want to do this, you must first change myError to the exported MyError).
What should the caller do when judging which type of error is the specific error of the custom error? MyError is not exposed to the outside package. The answer is achieved by exposing the method of checking for error behavior outside the package.
(err) ...
Or it is determined by comparing whether the error itself is equal to the constant error exposed to the package, for example, when operating files, it is often used to determine whether the file ends.
Similarly, there are error constants exposed by various open source packages.
if err != { return err }
Common errors made in error handling
Let's first look at a simple program to see if you can find some subtle problems
func WriteAll(w , buf []byte) error { _, err := (buf) if err != nil { ("unable to write:", err) // annotated error goes to log file return err // unannotated error returned to caller } return nil } func WriteConfig(w , conf *Config) error { buf, err := (conf) if err != nil { ("could not marshal config: %v", err) return err } if err := WriteAll(w, buf); err != nil { ("could not write config: %v", err) return err } return nil } func main() { err := WriteConfig(f, &conf) (err) // }
Two common problems in error handling
The error handling of the above program exposes two problems:
1. After an error occurs in the underlying function WriteAll, in addition to returning an error to the upper layer, it also records the error to the log. The upper layer caller does the same thing, records the log and returns the error to the top layer of the program.
So I get a bunch of duplicate content in the log file
unable to write:
could not write config:
...
2. At the top of the program, although the original error was obtained, there was no relevant content. In other words, the information recorded by WriteAll and WriteConfig in the log was not wrapped into the error and returned to the upper layer.
The solution to these two problems can be to add context information to the errors that occur in the underlying functions WriteAll and WriteConfig, and then return the errors to the upper layer, and the upper layer program finally handles these errors.
A simple way to wrap the error is to use functions to add information to the error.
func WriteConfig(w , conf *Config) error { buf, err := (conf) if err != nil { return ("could not marshal config: %v", err) } if err := WriteAll(w, buf); err != nil { return ("could not write config: %v", err) } return nil } func WriteAll(w , buf []byte) error { _, err := (buf) if err != nil { return ("write failed: %v", err) } return nil }
Attach the following information to the error
It just adds simple annotation information to the error. If you want to add the error call stack while adding the information, you can use the error wrapping ability provided by the /pkg/errors package.
//Only new information is attachedfunc WithMessage(err error, message string) error //Only append call stack informationfunc WithStack(err error) error //Add stack and information at the same timefunc Wrap(err error, message string) error
If there is a packaging method, there is a corresponding unpacking method. The Cause method will return the most original error corresponding to the packaging error - that is, it will unpack recursively.
func Cause(err error) error
The following is the error handler after rewriting using /pkg/errors
func ReadFile(path string) ([]byte, error) { f, err := (path) if err != nil { return nil, (err, "open failed") } defer () buf, err := (f) if err != nil { return nil, (err, "read failed") } return buf, nil } func ReadConfig() ([]byte, error) { home := ("HOME") config, err := ReadFile((home, ".")) return config, (err, "could not read config") } func main() { _, err := ReadConfig() if err != nil { ("original error: %T %v\n", (err), (err)) ("stack trace:\n%+v\n", err) (1) } }
The %+v used to format the string above is to expand the value based on %v, that is, expand the composite type value, such as the field value of the structure and other details.
This not only adds call stack information to the error, but also retains references to the original error. Cause can be restored to the initial cause of the error.
Summarize
To sum up, the principle of error handling is:
Errors are processed only once at the outermost layer of logic, and the bottom layer only returns errors.
In addition to returning errors, the underlying layer should wrap the original errors and add error information, call stack and other context information that is conducive to troubleshooting.
This is the end of this article about Go program error handling. For more information about Go program error handling, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!