People who write Go tend to have an opinion about its error-handling pattern. According to different language experience, people may have different ways of customary processing. That’s why I decided to write this article, although a little opinionated, I thought it would be useful to listen to my experience. The main problem I want to address is that it’s hard to enforce good error handling practices, errors often don’t have stack traces, and error handling itself is too verbose. However, I have seen some potential solutions that might help solve some of the problems.

Quick comparisons with other languages

In Go, all errors are values. Because of this, quite a few functions end up returning an error that looks something like this:

func (s *SomeStruct) Function() (string, error)Copy the code

As a result, the calling code will often use if statements to check them:

bytes, err := someStruct.Function() if err ! = nil { // Process error }Copy the code

Another approach is the try-catch pattern used in other languages such as Java, C#, Javascript, Objective C, Python, and so on. Below you can see Java code similar to the previous Go example, declaring throws instead of returning error:

public String function() throws ExceptionCopy the code

It uses try-catch instead of if err! = nil:

try {
  String result = someObject.function()
  // continue logic
}
catch (Exception e) {
  // process exception
}Copy the code

Of course, there are other differences. For example, Error will not crash your program, whereas Exception will. There are others, which will be covered in this article.

Implement centralized error handling

Take a step back and look at why and how to handle errors in one centralized place.

An example that most people might be familiar with is Web services – we generate a 5XX error if some unexpected server-side error occurs. In Go, you might implement this:

func init() { http.HandleFunc("/users", viewUsers) http.HandleFunc("/companies", viewCompanies) } func viewUsers(w http.ResponseWriter, r *http.Request) { user // some code if err := userTemplate.Execute(w, user); err ! = nil { http.Error(w, err.Error(), 500) } } func viewCompanies(w http.ResponseWriter, r *http.Request) { companies = // some code if err := companiesTemplate.Execute(w, companies); err ! = nil { http.Error(w, err.Error(), 500) } }Copy the code

This is not a good solution because we have to repeatedly handle errors in all handlers. For better maintenance, it is best to handle errors in one place. Fortunately, in the official Go blog, Andrew Gerrand provides an alternative that works perfectly. We can create a Type to handle errors:

type appHandler func(http.ResponseWriter, *http.Request) error func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if err := fn(w, r); err ! = nil { http.Error(w, err.Error(), 500) } }Copy the code

This can be used as a wrapper to decorate our handler function:

func init() {
    http.Handle("/users", appHandler(viewUsers))
    http.Handle("/companies", appHandler(viewCompanies))
}Copy the code

The next thing we need to do is change the handler’s signature to make them return errors. This approach is good because we’ve followed the DRY principle and haven’t reused unnecessary code – now we can return default errors in a separate place.

Error context

In the previous example, we could have received many potential errors, any one of which could have been generated in many sections of the call stack. That’s where things get tricky.

To demonstrate this, we can extend our handler function. It might look like this, because template execution isn’t the only place where something goes wrong:

func viewUsers(w http.ResponseWriter, r *http.Request) error { user, err := findUser(r.formValue("id")) if err ! = nil { return err; } return userTemplate.Execute(w, user); }Copy the code

The chain of calls can be quite deep, and various errors can be instantiated in different places throughout the process. This article by Russ Cox explains best practices for avoiding too many of these problems:

“Part of the convention for error reporting in Go is that the function contains the relevant context, including the operation being tried (such as the function name and its arguments).”

The example given here is a call to the OS package:

err := os.Remove("/tmp/nonexist")
fmt.Println(err)Copy the code

It will print:

remove /tmp/nonexist: no such file or directoryCopy the code

In summary, after execution, the output is the function called, the given argument, and the specific error message. You can also follow this practice when creating an Exception message in other languages. If we stick to this in our viewUsers handling, we can almost always figure out the cause of the error.

The problem comes from people who don’t follow this best practice, and you often see these messages in third-party Go libraries:

Oh no I brokeCopy the code

This doesn’t help – you can’t understand the context, which makes debugging difficult. Worse, when these errors are ignored or returned, they are backed up on the stack until they are processed:

if err ! = nil { return err }Copy the code

This means that when the error occurred was not passed on.

It should be noted that all of these errors can occur in an Exception-driven model – bad error messages, hidden exceptions, and so on. So why do I think this model is more useful?

Even if we’re dealing with a bad exception message, we can still see where it happened in the call stack. Because of the stack trace, this brings up some parts that I didn’t know about Go – you know that panic in Go includes a stack trace, but Error doesn’t. I speculate that it is possible that panic causes your program to crash and therefore requires a stack trace, while handling errors does not because it assumes you did something where it happened.

So let’s go back to our previous example – a third-party library with a bad error message that just prints out the call chain. Do you think debugging will be easier?

panic: Oh no I broke
[signal 0xb code=0x1 addr=0x0 pc=0xfc90f]

goroutine 1103 [running]:
panic(0x4bed00, 0xc82000c0b0)
/usr/local/go/src/runtime/panic.go:481 +0x3e6
github.com/Org/app/core.(_app).captureRequest(0xc820163340, 0x0, 0x55bd50, 0x0, 0x0)
/home/ubuntu/.go_workspace/src/github.com/Org/App/core/main.go:313 +0x12cf
github.com/Org/app/core.(_app).processRequest(0xc820163340, 0xc82064e1c0, 0xc82002aab8, 0x1)
/home/ubuntu/.go_workspace/src/github.com/Org/App/core/main.go:203 +0xb6
github.com/Org/app/core.NewProxy.func2(0xc82064e1c0, 0xc820bb2000, 0xc820bb2000, 0x1)
/home/ubuntu/.go_workspace/src/github.com/Org/App/core/proxy.go:51 +0x2a
github.com/Org/app/core/vendor/github.com/rusenask/goproxy.FuncReqHandler.Handle(0xc820da36e0, 0xc82064e1c0, 0xc820bb2000, 0xc5001, 0xc820b4a0a0)
/home/ubuntu/.go_workspace/src/github.com/Org/app/core/vendor/github.com/rusenask/goproxy/actions.go:19 +0x30Copy the code

I think this is probably something that goes unnoticed in Go’s design – not every language does.

If we use Java as a casual example, one of the stupidest mistakes people make is not logging stack traces:

Logger.error (ex.getMessage()) // Does not record stack trace logger.error (ex.getMessage(), ex) // Records stack traceCopy the code

But Go doesn’t seem to have that information in its design.

In terms of getting context information – Russ also mentioned that the community is discussing some potential interfaces for stripping context errors. It might be interesting to know more about this.

Stack trace problem solution

Fortunately, after doing some searching, I found this excellent Go error library to help solve this problem, to add a stack trace to errors:

if errors.Is(err, crashy.Crashed) {
  fmt.Println(err.(*errors.Error).ErrorStack())
}Copy the code

However, I think this feature would be an improvement if it became first class citizenship in the language, so you don’t have to make some type changes. Also, if we use a third-party library like the previous example, it might not use Crashy – we still have the same problem.

What should we do about mistakes?

We also have to think about what should happen when something goes wrong. This must be useful, they won’t crash your program and will usually deal with them immediately:

err := method() if err ! = nil { // some logic that I must do now in the event of an error! }Copy the code

What happens if we want to call a lot of methods, they generate errors, and then handle all the errors in one place? It looks something like this:

err := doSomething() if err ! = nil { // handle the error here } func doSomething() error { err := someMethod() if err ! = nil { return err } err = someOther() if err ! = nil { return err } someOtherMethod() }Copy the code

This feels a bit redundant, as in other languages you can process multiple statements as a whole.

try {
  someMethod()
  someOther()
  someOtherMethod()
}
catch (Exception e) {
  // process exception
}Copy the code

Or simply pass an error in the method signature:

public void doSomething() throws SomeErrorToPropogate {
  someMethod()
  someOther()
  someOtherMethod()
}Copy the code

I personally think these two examples accomplish one thing, except that Exception mode is less redundant and more resilient. If anything, I think if err! = nil feels like boilerplate. Maybe there’s a way to clean it up?

Handle errors as a whole when multiple statements fail

First, I did some more reading and found a more pragmatic solution in Rob Pike’s Go blog.

He defines a structure that encapsulates the wrong method:

type errWriter struct { w io.Writer err error } func (ew *errWriter) write(buf []byte) { if ew.err ! = nil { return } _, ew.err = ew.w.Write(buf) }Copy the code

Let’s do this:

ew := &errWriter{w: fd} ew.write(p0[a:b]) ew.write(p1[c:d]) ew.write(p2[e:f]) // and so on if ew.err ! = nil { return ew.err }Copy the code

This was also a good solution, but I felt there was something missing – because we couldn’t reuse this pattern. If we want a method that takes a string argument, we have to change the function signature. Or what if we don’t want to do a write? We can try to make it more generic:

type errWrapper struct {
    err error
}Copy the code
func (ew *errWrapper) do(f func() error) { if ew.err ! = nil { return } ew.err = f(); }Copy the code

But we have the same problem, if we want to call a function with different parameters, it won’t compile. However, you can simply wrap these function calls:

w := &errWrapper{} w.do(func() error { return someFunction(1, 2); }) w.do(func() error { return otherFunction("foo"); }) err := w.err if err ! = nil { // process error here }Copy the code

This works, but it doesn’t help much because it ends up being better than the standard if ERR! = nil checks bring more redundancy. If anyone has other solutions, I’d be interested to hear about them. Perhaps the language itself needs some way to pass or compose errors in a less bloated way – but it feels as if it is designed not to do so.

conclusion

After reading this, you might think I’m picking holes in Error and, by inference, I’m against Go. That’s not the case, I’m just comparing it to my experience with the try catch model. It is a good language for system programming, and some excellent tools have emerged. Kubernetes, Docker, Terraform, Hoverfly, to name a few. There are also small, high-performance, local binary advantages. Error, however, is hard to adapt. I hope my reasoning makes sense and that some schemes and solutions might be helpful.


About the author:

Andrew is a consultant at OpenCredo and joined the company in 2015. Andrew has worked for many years in a variety of industries, developing Web-based enterprise applications.


Via: opencredo.com/why-i-dont-…

Translated by geekpi and proofread by jasminepeng

This article is originally compiled by LCTT and released in Linux China