That’s a welcome feature even though I don’t like the syntax.
But Go could become the perfect language if they just fixed error handling and a couple small annoying quirks.
I've never seen a person who writes Go for more than a few weeks complain about the error handling. I certainly don't mind it myself. Is it really a problem people have or just somewhat of a meme at this point?
I used Go for about six months and eventually abandoned it to pursue Rust, a decision I've been extremely satisfied with. The longer I used Go, the more I grew to hate it and error handling was one component of that.
Well over half of Go source code in practice is dealing with errors, and somehow the Go ecosystem has convinced themselves that "verbose" is the same as "explicit" when it doesn't need to be. The worst problem isn't that it's just a lot of excess code, it's that it makes all sorts of very simple and common programming tasks ridiculously unwieldy. The most obvious example is calling a fallible method, doing something to the result, and returning it (or the error). This is one single character in Rust but a minimum of four lines—with branching—of copy-pasted boilerplate in Go. Which isn't a lot in the abstract, but then you multiply that by hundreds of times and now I have read, lex, parse, and mentally discard the majority of pages of source code that's doing something that could be done in ten lines with a massive incerase of clarity in a more reasonable language.
You've probably "never seen" us because we felt very let down by the overpromise and underdelivery of go and we left.
There's so much wrong in just this once sentence it's going to take a surprising amount of text to cover it all.
First, if you want to add extra annotations or scope to the error, you can actually do so—and trivially—while still using that single `?`. Widely-used error crates like `thiserror` allow you to specify that (for example) an I/O error will be automatically wrapped with `?` by some custom error type specific to your crate that conveys more information about what went wrong. This is phenomenal for errors that need to be bubbled up to end-users.
Second, for the majority of errors that are normal, expected, and recoverable, annotating them is just pointless busywork since they'll never be visible from outside of your program. For example, errors that eventually bubble up to an `.ok_or(...)` receive zero benefit from being annotated.
Third, is your preferred alternative the Go approach where you function as a less-capable human exception handler? Having to hunt through the source to identify what actually happened through some contortionist `error: thing went wrong: subsystem died: api client failed: gcloud client: cache error: filesystem error: file not found: tmp.VRVcBX1j` with no line numbers or function names, and various random components of the error string coming from either third-party libraries or the golang standard library? This is just so comically terrible to anyone who's spent time in languages with decent error handling it's genuinely hard to believe that people regularly come to its defense.
But of course I'm being generous here when we both know the actual status quo in the overwhelming majority of production Go projects is to simply bubble up the error with `return nil, err` with no context whatsoever, so you just get `error: file not found: tmp.VRVcBX1j` with absolutely no idea of where it came from. Those are always my favorite.
So, to recap: with Rust's `?` operator you actually can have your cake and eat it too. You can add library-specific context to your errors while actually wrapping the underlying error and not merely mashing strings together. You can opt into stack traces for your own code if you want to. And you can skip the annotations for code where you handle errors and don't bubble them up. The only apparent downside is that it's not overly verbose enough for Go adherents.
My biggest complaint about go error handling is that it's impossible to enumerate all of the errors that a function has returned. I have a use case to translate these into user facing errors for external use and find it a nightmare to enumerate them all.
What do you mean by "enumerate?" The error interface has exactly one member of type string. If a function is returning an error to you, it's specifying its own human readable error message. What's wrong with log.Fatal(err)?
I like Go. It's useful for the things I need it for since it compiles fast into a single binary and has networking utilities in its standard library. I was used to Rust's error handling when I started, but I liked how simple Go's design was in comparison, so I stuck with it to get a proper feel for the language.
After a while, I tried using the Goland IDE, and its static analysis tool found a dozen places where I wasn't handling errors correctly: I was calling functions that return errors (such as `io.ReadCloser.Close` or `http.ResponseWriter.Write`) without assigning their results to variables, so any errors produced by them would simply be ignored. My code was compiler-error-free, go-vet warning free, and still, I was shipping buggy code.
A few months later, I try using the golangci-lint suite of linters, and again, it found even more places where I wasn't handling errors correctly: I was assigning to `err` and then, later, re-assigning to `err` without checking if there was an error in between. My code was still compiler-error-free, go-vet warning free, and now IDE-warning free — and I was still shipping buggy code.
I don't see how anyone can see this as anything other than a big ugly wart on the face of the language. It's not because it's repetitive, it's because it's fragile. Even with code I was looking at and editing regularly, it was far too easy to get wrong. I'm going to continue using Go because it still fits my purposes well, but I'm only running it on my servers, so any mistakes I make are on my head, rather than on anybody else's.
I also don't think Go's design is really amenable to things like the Option and Result types people are writing — yes, I would never have had these problems in Rust, but code written using them in Go is clunky and looks out-of-place and doesn't feel like it's the right thing to write. I wouldn't ever use the `Optional` type in the article. But it's definitely not a solution in search of a problem. There's a huge problem.
So wait, go has handle errors by returning them, but it also doesn't force you to actually handle all return values? I thought that was the entire point of implementing error handling like that.
How are we still repeating the same mistakes C made 50 years ago?
You're sort of forced to handle them, in that if a function returns (Data, error), you need to assign the error to a variable (or do data, _ := func(), but at least that indicates you're intentionally ignoring the error), and since Go treats unused variables as a compile failure, you might as well check them properly. But there's nothing that says you must check them, no.
Ah thanks, that's kind of what I thought. I've read examples of Go but haven't written a line of it myself. the way GP described it made it sound like foo, err = bar() was merely a convention and you could do foo = bar() and drop err. If you have to add ", _" that's fine.
I'd be really happy with that! Building the functionality of errcheck[1] and ineffassign[2] into the compiler — or at the very least, into govet — would go a long way to allay my worries with Go.
I think the reason they don't do this is that it's a slight (albeit a very tiny one) against Go's philosophy of errors being values, just like any other. While the `error` type is standard and used throughout Go source code, it still just has a simple three-line definition[3] and is not treated as a special case anywhere else; there is nothing stopping you from returning your own error type if you wish. A third-party linter could simply check for the `error` type specifically, but the first-party tools should not, and there's nothing like Rust's `#[must_use]` attribute that could be used instead. I respect Go's philosophy, but I feel like pragmatism must win in this case.
Why should we have to waste our time doing something the machine can do? I normally only care about handling errors in 5% of places. The rest of the time it's just returning them. Life's too short
It might also be self-selection that people that truly dislike the error handling simply avoid golang. I’d really be interested to see how well go generics handle the Result type.
Can you elaborate on "fix[ing] error handling" and what some of those small annoying quirks might be? I've got my own annoyances, but am always interested in what other people think.