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).
This change also works really well with the new error
idioms... but that's for the next post in the series!
Top comments (22)
Eventually you do something like this:
Which I've seen in some other examples as:
To be honest, while this proposed solution works, it still looks like we only saving a few keystrokes. It's good but not great.
It's totally different. By your way, you can stop the subsequent operations when an error occurs
That doesn't work for returning errors, though. What if I wanted to return a wrapped version of the error?
Fair enough, but in that case, the wrapping is implicit and it looks like a twisted version of a try/catch block, something that spreads confusion.
I would prefer something like this:
As its more structured, easier to read and less confusing than the draft proposal. Note that the handle could be triggered multiple times just as the original handle version.
But now there's very implicit error checking, you don't know which lines may result in an error, which is something the proposal was trying to avoid.
tldr: go maintainers stubbornly refuse to admit that nil and not having union types are their two biggest mistakes.
Mistakes that are unfixable at this point.
From the Go 2017 user survey:
Union/Sum types are never mentioned (although "types" is)... Honestly I think I can live fine without sum types
Error handling, dependency management, and generics are by far the most mentioned things, so those are what they are focusing on.
Honestly I think you have never used them before, aren't you?
Uhhh nope, can't say I've ever had the desire to use them though
I know some people really adore them, and I know they can be useful, but in Go we've got interfaces and maybe even contract-based generics soon, which honestly kinda makes it so we don't really have a large need for union types 🤷♂️
You doesn't talk like a coder. Real coder will answer "Nope, I used them" or "Indeed, I didn't use them". I don't understand you.
Are you sure about "uhhh nope"? Visitor pattern destroys readability (and performance for short unions replacement - and we actually need only short unions), other interface based approaches in Go don't provide type safety.
generics are orthogonal to to algebraic/variadic data types. They become much better with generics though and can provide full type safety for error handling (unlike current approach).
I am afraid you are actually have a little sense about the idea.
Don't waste your time arguing. Go is designed to minimize amount of computer science needed to do work. It is NOT designed for productivity or engineer happiness. It is specifically designed to make developers easily replaceable.
Hi Dean,
I feel like adding handle fallbacks is limiting my chance to spot which place exactly generated the error.
Originally we could generate different errors for various reason. The example above is actually using same messages all the time. I know we have the line number, but I still preferred the "copyfile os.open error %s" format to specify exact spot individually.
I am not saying this won't allow it, it will just look very similar.
I would love it we would have also name of function passed to handler, ie:
What do you think ?
I actually quite like this idea! I'd suggest heading over to the wiki and put it there!
Hi Dean,
I'm a little conflicted on this.
First: instead of having the pattern
var, err := method()
now the code will be "littered" withvar := check method()
. I hope they can find a way to make thatcheck
implicit. I'm still not sure how I feel about thosehandle
blocks, I've only skimmed the draft yesterday.Second: I like this reaction about the whole thing:
Third: about
handle
blocks: think about reading the code after you wrote it, maybe weeks after. You read the code, and instead of seeing how the error is handled right there you need to jump up (hopefully not many levels) to see the various conditions, then you can resume reading the code, then you see another check so you go back up with your eyes to see what happens again with the error, then you resume, then you go back up again...ps. you can add syntax highlighting to the code blocks, like:
this way:
Implicit error checking is just kinda... bad. As a quote from the draft -
In terms of that reaction, I actually like that. Once a
handle
leaves scope, it should no longer be used.I also think that
defer
should work the same way. Adefer
should execute when it leaves its scope, not when the function is done executing. It just makes more sense in my eyes and is much more intuitive.For the syntax highlighting, I used a capital "G" on accident, I'll fix that!
I meant, implicit usage of check (you can derive which functions need a check if one of the return values is
error
) but still with explicit error handling.But now, while writing this, I think I understood. If they remove the explicit
check
there's no way for the programmer to instantly figure out which method calls can trigger an error, unless they inspect the source code.Using
check
also allows the compiler to throw errors if someone forgot to addcheck
.Although I’m not a go developer I love to check out new languages and concepts they bring. I never really liked the cluttering aspect of error handling in go but this check handle pattern is really confusing.
In the examples from another comment, there were multiple handle blocks. Which one is going to be used? How can I handle two different errors in the same scope differently. Why is the handle block before the check block?
To answer your questions -
The
handle
blocks chain. Once anhandle
block is reached, it becomes part of the chain. If an error occurs, the blocks execute from the bottommost block to the topmost. (It is proposed to remove the chaining functionality, though)If you simply want to add functionality, just add another handle block when you want that functionality to start being used. (Like the file-copy example I have in the post). Otherwise you'll have to do the standard
if err != nil
There's a few reasons behind this:
a. It's easier to think about needing to "declare" your handle blocks. You can't use a handle block if it hasn't been declared yet!
b. It makes for much faster compile times to require handle blocks.
c. It makes it more similar to how
defer
works (if you don't know Go this doesn't mean much)d. Enabling a handle block to be after a check block means that the handle block may have more variables in it's scope than what was at the check block...
Thanks for your elaborate reply.
Maybe I shouldn’t do this, but in my head I compare this to the try/catch/finally concept. Catch comes after try but cannot use any variables that have not been declared before try. But it makes the flow much clearer since you usually read code from top to bottom.
And one of my questions was how to implement multiple catch blocks. With try/catch i can have muktiple of those constructs or even multiple catch blocks. Let’s say I have file operation and a parsing operation and I want to handle them differently, I could declare two different catch blocks. How would I do this with check/handle?
It would look something like this:
In current Go with the handle construct would look something like this:
Note that the
notExist, ok := err.(*os.ErrNotExist)
is how you typecast in Go. Also, a semicolon in an if statement means "just execute the left side normally and evaluate the right side as the condition", although any variables declared on the left side have block scope within the if statement.Errors in Go are just an interface, so you just cast them to the error that you want to check.
When go2 will be released?
They've said Go 2 will probably be around Go 1.15 or so. So there's still a long time, Go 2 is still in very early planning phase, and they take their time.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.