4 ms·
This is an extreme intellectualisation of Go's error handling, which is literally just "errors are nullable strings returned by functions, and you have to check
by samhw 4y ago
This is an extreme intellectualisation of Go's error handling, which is literally just "errors are nullable strings returned by functions, and you have to check for them"[0]. I can't see where Go has learned from anything in PL research or practice, and indeed it's famous for not having learned: errors can't be matched; any hacky matching solution is unhelped by the type system; nor does the type system enforce checking for an error before accessing a value; adding context to errors is an absolute clusterfuck (in the absence of anything that could be called a type system) and therefore you end up with a load of "failed: op failed: encountered error: service could not load foo: error: database error: EAGAIN".
I just can't make sense of your argument. You say Go has learned some lesson from Java's checked exceptions, because ... errors are return values. What does that have to do with checked exceptions? Do you mean to contrast it with exceptions themselves? It certainly seems a complete non sequitur in relation to the (type) checking of exceptions. If your point is the "exceptions vs values" argument, then that's certainly one that's been hashed out at length, but I fail to see how picking one approach - of the two only approaches that any language can pick, both of which countless languages have used - is evidence of some kind of deep thought and originality.
- klodolph 4y ago> literally just "errors are nullable strings returned by functions, and you have to check for them" That’s definitely not how errors work in Go, either in theory or practice. In Go, I would characterize errors as an open union type. That’s how I think about them. They turn into strings when you log them. There is the type of error you get from errors.New. This is not the same as a string, because it is compared using pointer equality, not string equality. For example, n, err := f.Read(buf) if err == io.EOF { // ... } This is not a string comparison. > …errors can't be matched… You use ==, type assertion, typeswitch, or errors.Is/As/Unwrap is how you match errors in Go. Occasionally you will see a helper function (e.g. os.IsNotExist) but I think these functions are only there because they predate the errors.Is/As/Unwrap interface. > nor does the type system enforce checking for an error before accessing a value This sounds like some kind of dogmatic analysis not founded in PL theory. PL theory does not say “it is better for the type system to enforce checking”, because PL theory is not prescriptive. > adding context to errors is an absolute clusterfuck […] "failed: op failed: encountered error: service could not load foo: error: database error: EAGAIN". The nice thing about that is that you can then just do this: if errors.Is(err, syscall.EAGAIN) { // handle EAGAIN } Admittedly, producing good errors requires the programmer to care, and most programmers simply don’t (an observation). The error you wrote is not what errors in the applications I work with look like. I wrap errors with context when the context is relevant, unwrap context from errors if the context is implicitly understood. Equally, you will find that programmers don’t care in other languages, like Java, Haskell, or Ruby. When a programmer doesn’t care about errors in Java, you get unreadable stack traces, devoid of context. When a programmer doesn’t care about errors in Haskell, you get partial functions, which throw exceptions (which must be caught in the IO monad) even though the functions are declared to be pure. Go’s outcomes here seem pretty good to me. > You say Go has learned some lesson from Java's checked exceptions, because ... errors are return values. What does that have to do with checked exceptions? The default location to handle exceptions is at a distance. The default location to handle error return values is where they are returned. Checked exceptions are an attempt to bring exception handling closer to the location of the error, but they had ergonomic problems. The ergonomic problems with Java checked exceptions have been discussed elsewhere. > …I fail to see how picking one approach - of the two only approaches that any language can pick, both of which countless languages have used - is evidence of some kind of deep thought and originality. “Originality” is what you want to see in research languages like Haskell. That’s the frontier where new ideas are tested. However, speaking as a longtime Haskell programmer, it can often be damn hard to get work done in Haskell. Same thing with Rust—lots of good ideas, can be hard to get work done. Go is a more conservative approach. “Conservative” does not mean “better” and nor does it mean “worse”, it’s just another niche for programming languages.
- samhw 4y ago> That’s definitely not how errors work in Go, either in theory or practice. Sorry, I originally had a footnote after that, which I mistakenly removed: something to the effect that "yes, I realise errors are actually nullable structs implementing an interface which can return a string". Not quite as detailed as your comment, and perhaps incorrect in some respects, but in short: yes I understand errors aren't literally strings in Go, but I wanted to focus on the essential aspect, not the distinction between 'is a string' and 'is an interface type whose only common denominator is the string it returns'. (FWIW, I wrote Go professionally for several (painful, stagnant) years, so you can safely assume I have at least an intermediate familiarity with the language, and that any simplifications are just that.) And yes, I'm aware you can do type assertions on errors. That's why I specified "any hacky matching solution [i.e. type assertions] is unhelped by the type system": in other words, you can type-assert on some random types if you know that's what the function can return, but the type system neither (a) tells you what errors the function can return (like Java does with checked exceptions, or Rust - among many others - does with the errors-as-values approach) nor (b) enforces that you have handled all possible error types. In practice, this is a moot point because most Go code (that I've seen) doesn't really use the type system to structure its error handling anyway. If you look through any file at random in the Kubernetes source - probably the most vaunted example of 'well written Go code in a widely used application' – I can't find a single example of that being done, and very few examples of any error handling tout court besides "if err != nil { return [_,] err }". > The nice thing about that is that you can then just do this[: "if errors.Is(..."] That only works on errors created using the `errors` package. That also does runtime reflection on nullable pointers. > The error you wrote is not what errors in the applications I work with look like. Yeah, I'm used to the "it's ok if you ensure all the code you run is meticulously handwritten" attitude, from when I worked with Go. Unfortunately I have to use libraries, at which point that argument rather falls apart. That's why I prefer my programming language to enforce or encourage good habits, rather than simply saying "well it's your fault for being rubbish!" if my, or a dependency's, code is problematic for me. > “Originality” is what you want to see in research languages like Haskell. I'm not criticising it for being unoriginal. I'm saying that your reverential comment that "the Go language designers have taken some fairly sophisticated lessons from history" is rather over the top. I couldn't care less whether it's original or not, but you claimed it was, and I responded to that because it seemed a very odd claim (not because it's terribly important if true). --- Look, I appreciate the arguments for Go. It's a simple language that avoids all that clever, computer-sciencey, type-theory-y argle-bargle, for simple salt-of-the-earth craftsmen who just want to write Good Honest Application Code in a language where no one cares about that garbage collection stuff because we don't write garbage (and anyway what's a few milliseconds here or there!? what's determinism? http go brr!). I tried to convince myself of that for several years. The truth is that there are plenty of simple languages which aren't designed with obstinate inadequacies of the kind amply illustrated by the OP (though I'd throw in the horrific 'zero value' semantics, along with the bug-inducing, heap-smashing 'nullable pointer as option type' monkey patch that's semi-standard among Go programmers).