Go 2 is being drafted.

If you haven't heard, there will be a few major language changes coming to Go 2 . The Go team has recently submitted a draft of these changes, and as part of a 3-part series, I want to go through each part, explain it, and state my opinion on them! This is the first part of the series. After Error Handling, we will talk about Error Values, and then we can get to the big kahuna... Generics!

Remember that these are just drafts. They will most likely be added to the language, but there is still a chance that it won't. Also, remember that these drafts are not final, of course. The syntax will probably be a bit different from what it is right now.

These may not also be the only changes to the language, but these will probably be the only major changes.

So, what's the big deal with Error Handling?

Currently, if you want to check an error, you need to do if err != nil { ... . The construct is nice, but when you're working with any kind of I/O, your code starts to look something like this... (using the example from the draft)



// From the draft func CopyFile ( src , dst string ) error { r , err := os . Open ( src ) if err != nil { return fmt . Errorf ( "copy %s %s: %v" , src , dst , err ) } defer r . Close () w , err := os . Create ( dst ) if err != nil { return fmt . Errorf ( "copy %s %s: %v" , src , dst , err ) } if _ , err := io . Copy ( w , r ); err != nil { w . Close () os . Remove ( dst ) return fmt . Errorf ( "copy %s %s: %v" , src , dst , err ) } if err := w . Close (); err != nil { os . Remove ( dst ) return fmt . Errorf ( "copy %s %s: %v" , src , dst , err ) } }

We had to write fmt.Errorf("copy %s %s: %v", src, dst, err) three times, and w.Close() and os.Remove(dst) twice... and this is a pretty small function. So how can we fix this?

Introducing: check and handle !

check and handle will be new keywords, and you can kind-of think of them as a panic-defer-recover , but for use within a single function.

The format for check is check <expression> , where <expression> is either an error or a function call, where the last return value is an error. These are also expressions themselves, if the error is nil , then it returns all of the other arguments (ex: f := check os.Open(fileName) ).

If the error value is not nil, then check will jump to the handle blocks. To see what I mean, let's look at a simple example.

Go 1:



type Parsed struct { ... } func ParseJson ( name string ) ( Parsed , error ) { // Open the file f , err := os . Open ( name ) if err != nil { return fmt . Errorf ( "parsing json: %s %v" , name , err ) } defer f . Close () // Parse json into p var p Parsed err = json . NewDecoder ( f ) . Decode ( & p ) if err != nil { return fmt . Errorf ( "parsing json: %s %v" , name , err ) } return p }

Okay, so let's simplify this with check and handle !



type Parsed struct { ... } func ParseJson ( name string ) ( Parsed , error ) { handle err { return fmt . Errorf ( "parsing json: %s %v" , name , err ) } // Open the file f := check os . Open ( name ) defer f . Close () // Parse json into p var p Parsed check json . NewDecoder ( f ) . Decode ( & p ) return p }

How about that? We've trimmed down a lot of our boilerplate!

But what about that example from earlier... It had a lot more error handling! We had to clean up our files if they didn't copy properly... So how do we handle that? Let's take a look at the draft!



// From the draft func CopyFile ( src , dst string ) error { handle err { return fmt . Errorf ( "copy %s %s: %v" , src , dst , err ) } r := check os . Open ( src ) defer r . Close () w := check os . Create ( dst ) handle err { w . Close () os . Remove ( dst ) // (only if a check fails) } check io . Copy ( w , r ) check w . Close () return nil }

If you have multiple handle blocks, they will be executed from the bottom-most one to the top-most. This allows you to have more clean-up when an error occurs as a function flows.

Of course, this construct also works in functions which do not return errors, as noted in the draft!



// From the draft func main () { handle err { log . Fatal ( err ) } hex := check ioutil . ReadAll ( os . Stdin ) data := check parseHexdump ( string ( hex )) os . Stdout . Write ( data ) }

What do I think?

I think this is a really good change! My only real issue with it is that now check and handle become keywords, which are both variable names that aren't uncommon. I used to like the idea of collect (as illustrated in my previous blog post), although I think my view on collect changed after I wrote it. It just seemed like try-catch and didn't have a very intuitive syntax.

check-handle is still quite a bit like try-catch , although it is much more powerful. It allows more clean-up as the function progresses without adding more indentation. Every error check is also explicit, so you know where the exit points of the function are at a very quick glance.

The draft also contains a lot of details about what inspired this decision! They took a lot of inspiration from both Rust and Swift, which both have errors that must be checked explicitly (unlike Java, C++, Python, etc. which have implicit unwrapping of the stack).