12/08/2018, 15:51

Error Handling in Go

One of the things that have been brought up a lot in the Go community is error handling, while I must admit it is intimidating to test every possible errors using if/else, there are some techniques that you can use. The error interface Errors in Go are just a simple values that conform to ...

One of the things that have been brought up a lot in the Go community is error handling, while I must admit it is intimidating to test every possible errors using if/else, there are some techniques that you can use.

The error interface

Errors in Go are just a simple values that conform to error interface which define as follow:

type error interface {
    Error() string
}

Combine this with Go flexible type system making it really easy to define a custom error. Take for example a custom error to report back when user input wrong number.

type InvalidChoice int

func (e InvalidChoice) Error() string {
    return fmt.Sprintf("Invalid choice: %d", int(e))
}

Flow of controls

The flow of control is less clear in other languages. Take for example in Ruby

begin
  # normal execution is now nested
rescue => e
  # errror handling path
end

For simple things, it’s okay to have that nesting. But what if there’s additional nesting like if/else? It could quickly become hard to read. When writing Go code, prefer the form

f, err := os.Open(path)
if err != nil {
    // handle error
}
// normal execution

over

f, err := os.Open(path)
if err == nil {
    // normal execution
}
// handle error

This way, the error free case will read as a straight line down the page.

Using type switch

When writing Go code I often find myself did string comparisons of error messages to see what the error type was and man that was painful, luckily there was a better way. Remember that error is just a value and that we can define our custom error by implementing error interface, well leverage that with the type switch you can check for a specific type of error and handle those cases properly. For example:

err := doSomeWrite()
switch err.(type) {
case ErrBufferFull:
    // do something
case ErrNagativeCount:
    // do somehitng
case ErrBufferEmpty:
    // do somehitng
}

Remove repentition with function

Just because you can do

if err != nil {
  // handle error
}

doesn’t mean you have to do it everywhere in your code. Delegate error handling to a function or an object or in the simple cases you can make use of anonymous function to do encapulated error checking.

func doSomething() (err error) {
    f, err := os.Open(path)  
    write := func(b []byte) {
        if err != nil {
            return
        }
        _, err := f.Write(b)
    }
    
    write([]byte("Hello "))
    write([]byte("world!"))
    return
}

here the anonymous function was used to do write operation and as soon as there is an error it will become a no-op so that sub-sequence call to write will have no affect and the execution flow go as normal until it hit the return statement, in which case return the error value. Notice that here we make use of named return value so that the anonymous function can assign the error value from the write operation. Another example is to use helper function

func errCheck(err error) {
    if err != nil {
        log.Fatal("Error:", err)
    }
}

I admin that there are cases that these approaches might not be usable, but the important thing to note is that error is just a value and value can be program with, so you have the freedom to do as you wish.

0