The lack of exceptions just seems like a total bear to me. I wrote C for 10 years and did tons of stuff like:
Open file "foobar.dat". If that fails, abort with this error.
Read the first 4 bytes into x.
If that fails, abort with a different error.
Seek to byte x. If that fails, abort with yet another error.
and so on, and so on, over and over again. Python's exceptions are a huge improvement over this pattern of timidly asking permission to do anything. The fact is there are so, so many occasions where you want to abort a computation if one of the many steps along the way goes wrong. Something like Haskell's Maybe monad is a good way of attacking the problem too.
But Go has neither. It seems to just offer the bad old clumsy C way and say, "Deal with it." To those who have written real Go programs, I'm honestly wondering: how is this not a pain in the ass?
Unlike C, Go isn't overloading what is returned, it has an extra param, and the language has been baked to handle doing like initialization and checks in single line if statements. It does force the developer who might actually have a clue what the exception is and how to fix it to handle it, but IMHO this is a good thing. Go forces lots of things like this (not using an import, won't compile; not using a variable, won't compile).
Honestly, compared to the exception hellscapes I have had to deal with in Java and C++ --- it seems like the path of least surprise. Which incidentally has been my favorite things about Go, the low number of surprises.
A lot of using Go in real work has gone against my expectations. There are a lot of things I initially saw as huge warts (lack of exceptions, generics and import versions), but I liked channels enough (Erlang background) to give it a shot. So far, I have been delighted by using it as a stack (build cycle, deploy method, terse C'ish syntax).
The problem with this "forcing" that go does is that it ALSO includes _, which means it's inevitable that lazy developers will get tired of handling error and just shunt it into _.
You can't stop people from being lazy. Look at all the Java and Python code that forcefully ignores exceptions.
It is something that can be caught with static analysis, however. Someone recently put together an appropriate tool[1] for Go, in fact. It seems to work very well.
To be fair, the ability to use static analysis for error code checking is not something that is unique to Go. There was a paper recently on doing this for C (which found hundreds of bugs in the Linux kernel due to incorrect error code handling):
(Incidentally, the hard part of this analysis is not verifying that you checked the error, it's verifying that you propagated the error codes properly—that requires analyzing higher-order control flow.)
I'm not sure I like the idea of special casing _ specifically, but a similarly concise way of saying "if this fails, this thread of execution is FUBAR" would be great.
I'm not saying this is common -- and in fact the standard C library does a lot of the overloading you're talking about -- but for your own code and functions, you can separate out errors and return values, as shown above.
Writing exception-safe code is non-trivial. There has been a lot of debate recently about whether exceptions are a mis-feature. The main worry is that if you allow exceptions sensible-looking code can lead to many non-obvious bugs. This link gives some insight:
Go's choice I think is very well considered. If reliability is important to you then I believe handling errors explicitly leads to clearer, more correct code.
These criticisms of exception handling are primarily based on their implementation in C++; they are not issues inherent in exceptions. Exceptions are clearly more problematic when they are bolted-on after the fact to a language with manual memory management and a large existing body of code that is unaware of them.
But writing exception-safe code IS trivial, in basically any language except C++. Garbage collection is all you need in most cases, and try-with-resources/RAII/etc takes care of closing I/O.
Note that Go still needs defer() to reliably take care of the latter, so it's not clear to me what is gained by the omission (or rather, strong deprecation) of exceptions.
I'm writing a pretty big system in Go, and had not seen this as much of an issue. Could you provide more insight into your point? I'd like to see some code to compare, if its not to much to ask.
I'm not sure how great of an example it is, but I had the thought when recently rereading this routine from an old game, reading its level file:
static void scanlevel(int num, FILE *fp)
{
filemap_t filemap;
int i;
if (fseek(fp, mapptrs[num-1], SEEK_SET))
error("Can't load level %d from blockman.lvl", num);
if (fread(&filemap, sizeof (filemap_t), 1, fp) < 1)
error("Can't load level %d from blockman.lvl", num);
if (filemap.startx < 0 || filemap.startx >= LEVWIDTH
|| filemap.starty < 0 || filemap.starty >= LEVHEIGHT)
{
error("Level %d is corrupt", num);
}
map.startx = filemap.startx;
map.starty = filemap.starty;
for (i = 0; i < LEVWIDTH*LEVHEIGHT; i++)
{
map.tiles[i] = (tiletype_t)filemap.tiles[i];
if (map.tiles[i] < 0 || map.tiles[i] >= NUMTILES)
error("Level %d is corrupt", num);
}
}
This shows another benefit of exceptions, which is that if uncaught, they stop the program with a traceback of the exact point they occurred. So it's not even necessary to write most of the checks above. `error` here is a routine that aborts the program; you get that behavior by default with exceptions.
Whereas in C/Go if I forgot one of those error checks, the error would occur silently, leaving the program in some weird inconsistent state that I never planned for. It would just do something stupid and maybe crash or panic later on, far from the place where the initial error occurred.
I guess I'm just arguing for exceptions, which is old news as languages that have them have been around for quite a while. But Go doesn't offer much of a substitute of which I'm aware. The explanation of how it solves these problems has not been forthcoming.
I think the key difference is between assertions/"this should never happen" error checking, and actual error conditions that you want to pass back to client code, because it knows better than you do what the right thing to do is.
When you're writing reusable library code (and when you think about the scale of Google's codebase, they must have an insane amount of these libraries), it's important to make this distinction. There are some error conditions where you really just want to say "if this ever happens, just die, because there's nothing sane to be done", and Go provides panic() for these situations, similar to the error() function in your code above.
For situations where you do want to return a meaningful error to the client, I think Go's multiple return values provide a very good way to do it, far better than the overloading of NULL or -1 that you find in C and C++.
The list of cool things Go doesn't have is a darn long one. They opted for simplicity at lots of points. I think it is worth noting these were decisions, not necessarily oversights.
There are a lot of great languages that end up mostly academic because they lack whatever the magical balance of features, simplicity and usefulness it takes for a language get mind share.
I suspect Go might have hit the magical balance with channels, strong types, great build system, simple minimal syntax and language keywords, fairly opinionated best practices (and formatting) and static single file deploys.
Multiple return values are something completely different from sum types. Just because Go by convention returns errors as an additional return values where other languages prefer sum types, you shouldn't conflate the two.
Parent didn't say they were the same, rather they implied that sum types are better.
Sum types can handle multiple return values seamlessly in a typesafe way as a special case, but are not limited to that because they may have different data shapes other than simple products, and callers can be checked to deal with each possible shape at each call site by the compiler.
Claiming sth is "better" requires two things to be comparable, and thus, reasonable similarity in their resp. nature. Claiming something is better than something else implies this similarity (otherwise any comparison would be moot), which I refuted in this particular case.
On a side note, the same people who claim that sum types are "better" are never able to come up with a constructive proposal how sum types could be integrated into Go in an elegant way.
They are "better" in that they are, in fact, more constrained; only when the error case arises will there be any accessible error value; otherwise, the actual expected value will be found. Since go uses an ad hoc product type, you always get an error value and the return value, even if they are mutually exclusive most of the time.
Also, they are both ways to build larger types from smaller ones, and the way they go about doing it is rather obvious from their names, and thus the contrast.
> On a side note, the same people who claim that sum types are "better" are never able to come up with a constructive proposal how sum types could be integrated into Go in an elegant way.
Forgo the cutesy anonymous members for the massive benefits of sum types? For a team which prides itself for its ability to perform trade-offs, they sure were rigid in this stance.
> They are "better" in that they are, in fact, more constrained; only when the error case arises will there be any accessible error value; otherwise, the actual expected value will be found. Since go uses an ad hoc product type, you always get an error value and the return value, even if they are mutually exclusive most of the time.
But multiple return values are there for much more than returned result and error. You conflate that with the specific use of returning result and error, and based on that, you claim that sum types are better. That's a straw man par excellence.
Sum types and multiple return values are not mutually exclusive, though several languages today use tuples to emulate multiple return types (see Scala, Rust for examples).
I am not a language designer, but I have become interested in languages in the past couple of years. Sum types require some sort of generics implementation, which Go does not have. I think the design choices the authors made regarding the language have made adding generics that much harder, that now they are struggling to find the "Go way" of fitting them into the language.
The explanation of how it solves these problems has not been forthcoming.
It'd be interesting to see what you'd do with the code above in C++, and where you'd put the error handling code for diverse errors that might occur reading this particular file.
The Go approach is to handle errors locally, often in the calling method, which makes it clear where they are handled and what the outcome is, and easier to recover gracefully, without unexpected exceptions from code in libraries or other code in the program. Some large users of C++ (like Google) refuse to use C++ exceptions in their own code - so they are not entirely without controversy.
In the code above, if you used exceptions, and relied on the libraries to throw exceptions for errors, you'd have to throw your own exception at:
error("Level %d is corrupt", num);
So you'd have a mix places where exceptions were generated (in unknown lib code, in your code) and an unknown (for the reader) mix of places where they are handled. I'm sure this could be done gracefully, but it does mean errors missed might be handled at a much higher level in the code, far away from where they were generated, which can lead to errors being missed until it is too late to do anything but output a stack trace and exit, which to the user seems equally stupid as crashing or panicking at some later point.
If you exit the program on simple errors like being unable to load a single game file, it's not very pleasant for the user - I'd expect it instead to recover gracefully and show the user an error before continuing, which is easy enough when using Go's pattern of error returns, and harder with exceptions where you have unrolled the stack possibly past the loading code, unless you start handling exceptions in calling code one level up, which looks very much like the error handling of Go. So there are trade-offs to using either method aren't there?
Not using C++ exceptions in Google's code bases is partly due to historic reasons (since the original code base did not have it), and partly due to them being "harder" to implement in C++ correctly.
They even acknowledge in their C++ style guide[1] that: " Things would probably be different if we had to do it all over again from scratch."
I'm not clear on which error check you think you might "forget". If you're performing an operation that can fail, wouldn't that be a clue that you need to check for failure?
You can't actually write code like that in Go, by the way. It's not going to let you read data directly into a struct like that, nor should you really be doing so in the first place.
I'd be happy to provide a more detailed analysis, and possibly even Go-equivalent code, but without further context (at least the definitions for `filemap_t` and whatever struct type `map` is, if not a full description of the file format and its meaning), it's impractical.
> I'm not clear on which error check you think you might "forget". If you're performing an operation that can fail, wouldn't that be a clue that you need to check for failure?
Sure - and the possibility of exceeding the bounds of an array would be a clue that you need to bounds check, but there's still a hell of a lot of C code out there with array overflow errors. You can argue that people who make these errors are bad programmers, but that's fairly irrelevant - most programmers of any level will end up working with code with errors in it at some point. Exception stack traces are an extremely useful way to find out where something went wrong when someone failed to do some necessary error checking.
I have no experience with Go, so I'm not saying what it does is wrong - I'm just curious. Say a customer experiences a failure with your software caused by some missing/incorrect error handling, what do you do to work out what happened?
> the possibility of exceeding the bounds of an array
Not possible in Go, the runtime will panic.
> there's still a hell of a lot of C code out there with array overflow errors
An extremely easy error to make in many cases, which is why modern programming languages bounds-check.
> Exception stack traces
Go will give you a very nice stacktrace should it ever panic.
> I have no experience with Go
Which is really the problem. People keep arguing about Go's merits based on no substantive understanding.
It's very obvious which operations can fail without a panic in Go, because functions explicitly return error objects -- actual error objects, not magic numbers. The return signature for the Go equivalent of fread is (int, error), not (int).
It was never documented, but any long time Go user knows about it.
Looking at the code it seems it has been removed.
> ... I would certainly never enable such a thing in real code.
So I assume you don't do C or C++. :)
While I agree with you, there are certain cases where it might help. That is why most strong typed languages with native compilers allow to selectively disable bounds checking, since the Pascal/Modula-2 days.
I only support doing this if profiling proves it is really worth it, give the security issues.
If C had a simple universal switch for bounds checking, I'd turn it on everywhere and immediately revoke commit privileges for anyone on my team who turned it back off. But it doesn't, and necessarily can't, making your statement nothing more than an annoying exercise in wrongful pedantry. It is contextually obvious I was talking about Go code and/or languages/compilers with such a switch.
I was trying you out, because if you make such a statement then I am lead to believe you stay away from languages that don't provide control over bounds checking.
But Go has neither. It seems to just offer the bad old clumsy C way and say, "Deal with it." To those who have written real Go programs, I'm honestly wondering: how is this not a pain in the ass?