I disagree (I'm from the Elixir/Erlang "code the narrow happy path and accept failure as quickly as possible" camp) and want to see your empirical data that discarding exceptions was a good design decision. Because to me it seems like you would spend a lot of time writing unnecessary boilerplate code to check for errors everywhere (if you bother checking at all... and if you don't, you are begging to get into an inconsistent runtime state which could lead to extremely hard-to-trace bugs)
It is my informed opinion that all unexpected states are bad and should be logged and killed/restarted from a known state ASAP. (Basically, the Erlang philosophy.)
Here is my data: Here are the most-used words in Go https://anvaka.github.io/common-words/#?lang=go ("if err != nil return err," anyone?) and here are the most-used words in Elixir https://anvaka.github.io/common-words/#?lang=ex . Sure seems like a lot of Go code is written to handle these "exceptions" they're not having! ;)
I didn't actually come here to debate Go, but statements like yours without any evidence to back them up are basically a religious belief and absolutely nothing more.
If you were selling this language to me, how would you explain writing 33% more code to do the same business logic, just to handle errors, over something like Elixir with its rather astonishing uptime and response rate and almost no boilerplate error-handling code? For the backend of an API or webserver, hypothetically.
In either case, you should be handling errors.
Just found this comparison which is at least interesting: https://www.slant.co/versus/126/1540/~golang_vs_elixir
Compared to Java I have ported to Go, returning error + defer has led to much more localized (and this grokable) and less verbose error handling. Less verbose? I suppose this is not true if you throw exceptions and handle them all at the top (Go has panic for these kind of fatal events BTW), but if you regularly have try-catch-finally blocks, the Go approach is in fact less verbose.
Go should have had Rust-style enums that let you do things like Option<T> and Result<T,E>. This way, you are forbidden by the compiler from ever extracting an invalid value from a function that returned an error (or returned no value).
I really don't see a downside of Rust's approach compared to Go's after having programmed in both. But, then again, Rust's approach requires generics and I know Go's team doesn't want to go in that direction.
Nothing in Rust (or Go, even) ordinarily precludes the type of retry approach you're describing, of course.
I disagree:
https://github.com/golang/proposal/blob/master/design/15292-...
https://github.com/golang/go/issues/15292#issuecomment-22198...
Here is the comment you linked:
> Ian's proposal demands too much. We cannot possibly develop all the features at-once, it will exist in an unfinished state for many months.
> In the meantime, the unfinished project cannot be called official Go language until done because that will risk fragmenting the ecosystem.
> So the question is how to plan this.
> Also a huge part of the project would be developing the reference corpus. developing the actual generic collections, algorithms and other things in such a way we all agree on that they are idiomatic, while using the new go 2.0 features
The author of this comment is not a member of the core team, as far as I know, and the comment doesn't suggest the proposal was rejected.
And it's symmetric with function parameters. If you think multiple return values is a kludge, then for the same reasons so would multiple parameters. After all, you can always curry multiple parameters, or shoe-horn multiple parameters into a compound objet like a struct.
Now whether it makes sense to leverage multiple return values as a substitute for specialized error types, or for exceptions, is a different question. But if you don't see the value in multiple return values than I think it's hard to assess things objectively.
I program a lot in Lua and love multiple return values. And in Lua the standard practice is to leverage multiple return values with exceptions. E.g.
local x, y = assert(bar())
where on success bar is expected to return a first value that evaluates to true in a boolean context. If so then assert just returns that first value and any subsequent values. If not then it throws the second value as an exception, which usually is expected to be an error description string or an object that can be stringified, but assert doesn't care about the type.Note that assert() isn't a specialized function. You can implement it in pure Lua. Multiple parameters and multiple return values are generalized as lists (similar to Perl) which also can be used when, e.g., initializing a table.
function foo() return "one", "two", "three" end
for i,s in ipairs({ foo() }) do print(i, s) end
prints 1 one
2 two
3 three
Of course these things can be abused and they're not strictly _necessary_. But they're powerful abstractions that can prove very useful, especially in the context of functional programming styles.Go seems simple on the outside but is actually very elegant and sophisticated internally. Multiple return values, lexical closures that automatically capture by reference and which can escape their original scope without any fuss, lightweight stackful coroutines (stackful meaing no decoration required for recursively invoking yielding functions) that automatically adjust their stack size. You usually only see these things in interpreted and dynamically typed languages, if at all, because they can be very difficult to implement efficiently with an optimizing compiler. Similar concepts in C#, C++, Rust are not nearly as powerful yet still require syntactic kludges (like decoration) to help drive the kludgy implementations. That Go implements these things seamlessly, while compiling straight to native code, is quite a testament to the Go team.
Most of the attention these days is given to stronger typing systems. And that's fine. Go doesn't win too many points on that front, although its duck typing has unique advantages, even over traits. But it's not reasonable to overlook all the other things that Go does extremely well and almost without equal for an optimizing, AOT-compiled, strongly typed language.
All of these things come with trade offs. Typing systems are hardly the be all, end all of language design. They're usually not even the most interesting part, nor the part most relevant to productivity. What's unique about Rust, for example, isn't its typing system, but its borrow checker. And the borrow checker drives directly and indirectly much about their choices with other aspects of the language. Like I said, multiple return values are difficult to implement underneath the hood, and in a language like Rust might require some really odd declaration syntax and have other unforeseen consequences. Thus you see them reconceptualizing multiple return values in terms of a kind of polymorphic compound type. That's not intrinsically better--if anything it can be seen as a kludge--but in any event it's driven by other needs than pure abstract functionality and should be not only criticized that way, but appreciated that way.
s = append(s, f())
m[k] = f()
c <- f()
f().DoStuff()
h(f(), g())
work with single-valued functions but not multi-valued functions. The feature is not well integrated with the rest of the language.Because of constructors, I don't think they would have eschewed exceptions. Constructors don't return any value (in both C++ and Java ABI terms, they are implemented as void functions!) anyways, so having the "I don't know how to do what your arguments told me to do" be indicated via an exception being thrown is an intentional model rather then using return codes.
These things are typically done by exceptions and monads; the "zig" language has some interesting syntax around this kind of thing as well.
But just multiple return values isn't enough for me - too much "if error return error" noise for my taste.
This is such a big deal, I don't understand how it's defensible. Code is read dozens of times more often than it's written. When code looks like:
if val1, err := thingImActuallyTryingToDo1(); err != nil {
return nil, fmt.Errorf("thing failed: %v", err)
}
if val2, err := thingImActuallyTryingToDo2(val1); err != nil {
return nil, fmt.Errorf("thing failed differntly: %v", err)
}
if val3, err := thingImActuallyTryingToDo3(val3); err != nil {
return nil, fmt.Errorf("thing failed again: %v", err)
}
it's impossible to tell at a glance how the function is actually working. You have to parse out 90% garbage just to get to the details of what's really happening.Worse is that it's never this "clean" and repeatable in practice. It becomes a bunch of subtly-different variants on that theme (append if the function wasn't an error, or do something different if it errored, etc.) that the actual logic of your function is obscured in a maze of error-handling.
Code doesn't become easier to read/understand by just tucking away 90% of it (the hard/interesting parts at that, mind you).
If 90% of your code really is unique, specific action to error conditions, then by all means, this topic isn't for you.
But if 90% of your code is "stop processing in this function, log the error, and pass it up the stack", then why not abstract that away and reduce the noise? (i.e. something like exceptions or monads or arrows or ...)
After a while, you just learn to ignore the `if err != nil` and get on with your life. I mean, sure, it would still be nicer to have an abbreviation, but it's not like Go code is 90% `if err != nil` (as critics always try to frame), and when I read Go code, I'm most definitely not spending most (or any) of my time stumbling over them.
And most of your code winds up doing error checks because if any call in the stack under a particular function produces an error, it usually winds up having to bubble the entire way up the stack.
I understand that. I was pointing out this is less noise than try-catch-finally (assuming the programmer is comparing Go to C++/Java/C#). I don't deny some functional langs have more elegant handling in this respect.
If you want some data, "if err != nil return err" contains the four most frequently used words in Go code. See https://anvaka.github.io/common-words/#?lang=go . For comparion, try/catch are only 34th and 35th for Java, and even lower for C# and C++.
Every call that can fail, which isn't quite every function call.
DRY principle seems not applicable here.
But that would require either more special-case magic (always bad), or generics (not in 1.x, supposedly).
It would be great to have something like a conditional return to get rid of the endless `if err != nil`, or, while we're at it, from endless `.then` that futures-based programming ends up with:
foo_result, err = foo(...)
on_failure(err, "foo failed")
bar_result, err = bar(foo_result, ...)
on_failure(err, "bar failed though foo did not")
return bar_result + 1