Go Error best practices

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 YAMLSyntaxErrorio.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.Asif 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.ErrorCallFailederrors.Iserrors.Aserrors.Iserrors.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.Iserrors.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{})}// OutputEquality Operator: Both errors are not equalIs 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.Printflog.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.EOFsql.ErrNoRowserr:= 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