
The Go language has been repeatedly criticized for its lack of structure, and every error in Go needs to be handled, and errors are often nested. Structure such as the following:try...catch
a, err := fn()
if err != nil {
return err
}
func fn() error {
b, err := fn1()
if err != nil {
…
return err
}
if _, err = fn2(); err != nil {
…
}
}
copy the code
Go Error is also an interface
In the Go language, Go Error is also an interface:
type error interface {
Error() string
}
copy the code
so, implementing, creating, or throwing errors is actually implementing the interface. the three most common ways are:
errors.New
fmt.Errof
implement error interface
var ErrRecordNotExist = errors.New("record not exist")
func ErrFileNotExist(filename string) error {
return fmt.Errorf("%s file not exist", filename)
}
type ErrorCallFailed struct {
Funcname string
}
func (*ErrorCallFailed) Error() string {
return fmt.Sprintf(“call %s failed”, funcname)
}
var ErrGetFailed error = &ErrorCallFailed{ Funcname: "getName", }
copy the code
Go errors involve only the following two logics:
- throwing errors involves how we define errors. when implementing functionality, exceptions require a reasonable error to be returned
- handle errors. when calling a function, you need to implement different logic according to the return of the function, considering whether there is an error, whether the error belongs to a certain type, whether the error is ignored, and so on.
func (d *YAMLToJSONDecoder) Decode(into interface{}) error {
bytes, err := d.reader.Read()
if err != nil && err != io.EOF {
return err
}
if len(bytes) != 0 {
err := yaml.Unmarshal(bytes, into)
if err != nil {
return YAMLSyntaxError{err}
}
}
return err
}
type YAMLSyntaxError struct {
err error
}
func (e YAMLSyntaxError) Error() string {
return e.err.Error()
}
copy the code
This article in Kubernetes decode.go can not only return errors directly, but also wrap errors, either returning them or simply ignoring them YAMLSyntaxError
io.EOF
in general, there are three ways to determine the type of error:
- directly through , for example:
==
if err == ErrRecordNotExist {}
- type inference,
if _, ok := err.(*ErrRecordNotExist); ok {}
errors.Is
and Methods. Add starting with Go 1.13. Involved error wrapping, solving the trouble of locating nested errors.errors.As
if errors.Is(err, ErrRecordNotExist)
rules to follow
Once you understand the basic concepts of Go’s mistakes, it’s time to discuss the rules that can be followed for better practice. Let’s start with the definition and then to error handling.
definition error
- Use fmt. Errorf instead
errors.New
fmt.Errorf
provides stitching parameter capabilities and packs errors. while we found no difference between the two methods when dealing with simple errors, always setting it to your preference keeps the code uniform.fmt.Errorf
encapsulates the same error
encapsulating the same errors, such as those mentioned above, is a common code optimization that, in combination, can be unwrapped to better determine the true cause of the error. as for the difference between and, the former requires both type matching and message matching, while the latter only requires type matching.ErrorCallFailed
errors.Is
errors.As
errors.Is
errors.As
func fn(n string) error {
if _, err := get(n); err != nil {
return ErrorCallFailed("get n")
}
}
func abc() error {
_, err = fn("abc")
if err != nil {
return fmt.Errorf("handle abc failed, %w", err)
}
}
func main() {
_, err := abc()
if errors.Is(err, ErrorCallFailed){
log.Fatal("failed to call %v", err)
os.Exist(1)
}
}
copy the code
use %w instead of %v
When a method is called multiple times, in order to get a complete call chain, the developer will wrap it up layer by layer where the error is returned, by constantly adding the unique characteristics of the current call, which can be a log or a parameter. Occasionally using %v instead of %w in incorrect stitching will cause Go’s error wrapping feature to fail in Go1.13 and later. The types of errors after a correct line wrap are as followsfmt.Errorf

make the error message concise
reasonable error messages can keep us away from redundant information by wrapping them layer by layer.
many people have the habit of printing logs on the following things, adding parameters, the name of the current method, the name of the calling method, which is unnecessary.
func Fn(id string) error {
err := Fn1()
if err != nil {
return fmt.Errorf("Call Fn1 failed with id: %s, %w", id, err
}
...
return nil
}
copy the code
However, the clear error log contains only information about the current operation error, internal parameters and actions, and information that the caller does not know but the caller does not know, such as the current method and parameters. This is the error log for endpoints.go in Kubernetes, a very good example of printing only the internal Pod-related parameters and failed actions:Unable to get Pod
func (e *Controller) addPod(obj interface{}) {
pod := obj.(*v1.Pod)
services, err := e.serviceSelectorCache.GetPodServiceMemberships(e.serviceLister, pod)
if err != nil {
utilruntime.HandleError(fmt.Errorf("Unable to get pod %s/%s's service memberships: %v", pod.Namespace, pod.Name, err))
return
}
for key := range services {
e.queue.AddAfter(key, e.endpointUpdatesBatchPeriod)
}
}
copy the code
the golden five rules for dealing with error
the following describes what the author considers the golden five rules.
errors.Is
better than==
== is more error-prone and can only compare the current error type and cannot be unpacked. therefore, or is a better choice.errors.Is
errors.As
package main
import (
"errors"
"fmt"
)
type e1 struct{}
func (e e1) Error() string {
return "e1 happended"
}
func main() {
err1 := e1{}
err2 := e2()
if err1 == err2 {
fmt.Println("Equality Operator: Both errors are equal")
} else {
fmt.Println("Equality Operator: Both errors are not equal")
}
if errors.Is(err2, err1) {
fmt.Println("Is function: Both errors are equal")
}
}
func e2() error {
return fmt.Errorf("e2: %w", e1{})
}
// Output
Equality Operator: Both errors are not equal
Is function: Both errors are equal
copy the code
- prints the error log, but does not print the normal log
buf, err := json.Marshal(conf)
if err != nil {
log.Printf(“could not marshal config: %v”, err)
}
copy the code
a common mistake for newbies is to use the use of printing all logs, including error logs, which causes us to not be able to process logs correctly through the log level, which is difficult to debug. we can learn the correct method from the application’s dependencycheck.go.log.Printf
log.Fatalf
if len(args) != 1 {
log.Fatalf(“usage: dependencycheck <json-dep-file> (e.g. ‘go list -mod=vendor -test -deps -json ./vendor/…’)”)
}
if *restrict == “” {
log.Fatalf(“Must specify restricted regex pattern”)
}
depsPattern, err := regexp.Compile(*restrict)
if err != nil {
log.Fatalf(“Error compiling restricted dependencies regex: %v”, err)
}
copy the code
- never go through error handling logic
here is a description of the error process.
bytes, err := d.reader.Read()
if err != nil && err != io.EOF {
return err
}
row := db.QueryRow(“select name from user where id= ?”, 1)
err := row.Scan(&name)
if err != nil && err != sql.ErrNoRows{
return err
}
copy the code
as you can see, and both errors are ignored, which is a typical example of using error to represent business logic (data does not exist). i’m against such a design but support size optimization, which helps by adding return parameters instead of throwing errors directly.io.EOF
sql.ErrNoRows
err:= row.Scan(&name) if size == 0 {log.Println(“no data”) }
- the bottom method returns an error, and the upper method handles the error
func Write(w io.Writer, buf []byte) error {
_, err := w.Write(buf)
if err != nil {
log.Println(“unable to write:”, err)
return err
}
return nil
}
copy the code
there is an obvious problem with code similar to the above. if an error is returned after printing the log, there is a good chance that there will be duplicate logs because the caller may also print the log.
so how to avoid it? let each method perform only one function. a common choice here is that the underlying method only returns an error, and the upper-level method handles the error.
- wrap the error message and add context that facilitates troubleshooting.
There is no native stacktrace to rely on in Go, and we can only get stack information about those exceptions through our own implementation or third-party libraries. Kubernetes, for example, implements a more complex klog package to support log printing, stack information, and context. If you develop Kubernetes-related applications, such as Operators, you can refer to structured logging in Kubernetes. In addition, those third-party error encapsulation libraries such as pkg/errors are very well known.
epilogue
Go’s design philosophy was originally intended to simplify, but sometimes complicating things. However, you can never assume that Go error handling is useless, even if it’s not so user-friendly. At the very least, it’s a good design to return errors on an error-by-error basis, uniformly at the highest level of invocation. In addition, we can still expect that these improvements in the upcoming release will lead to simpler applications.
thanks for reading!
cite
https://go.dev/blog/go1.13-errors
https://errnil.substack.com/p/wrapping-errors-the-right-way
https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully