> Other features common in modern languages, like tagged unions or syntactic sugar for error-handling, have not been added to Go.
> It seems the Go development team has a high bar for adding features to the language. The end result is a language that forces you to write a lot of boilerplate code to implement logic that could be more succinctly expressed in another language.
Being able to implement logic more succinctly is not always a good thing. Take error handling syntactic sugar for example. Consider these two snippets:
let mut file = File::create("foo.txt")?;
and:
f, err := os.Create("filename.txt")
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
}
The first code is more succinct, but worse: there is no context added to the error (good luck debugging!).
Sometimes, being forced to write code in a verbose manner makes your code better.
Especially since the second example only gives you a stringly-typed error.
If you want to add 'proper' error types, wrapping them is just as difficult in Go and Rust (needing to implement `Error` in Go or `std::Error` in Rust). And, while we can argue about macro magic all day, the `thiserror` crate makes said boilerplate a non-issue and allows you to properly propagate strongly-typed errors with context when needed (and if you're not writing library code to be consumed by others, `anyhow` helps a lot too).
fmt.Errorf with %w directive in fact wraps an error. It will return an fmt.wrapError struct which can be inspected using `errors.Is`. So it's not stringly typed anymore.
I am fully aware of how fmt.Errorf works as well as what's inside the `errors` package in the Golang stdlib, as I do work with the language regularly.
In practice, this ends up with several issues (and I'm just as guilty of doing a bunch of them when I'm writing code not intended for public consumption, to be completely fair).
fmt.Errorf is stupid easy to use. There's a lot of Go code out there that just doesn't use anything else, and we really want to make sure we wrap errors to provide 'context' since there's no backtraces in errors (and nobody wants to force consuming code to pay that runtime cost for every error, given there's no standard way to indicate you want it).
errors.New can be used to create very basic errors, but since it gives you a single instance of a struct implementing `error` there's not a lot you can do with it.
The signature of a function only indicates that it returns `error`, we have to rely on the docs to tell users what specific errors they should expect. Now, to be fair, this is an issue for languages that use exception's - checked exceptions in Java notwithstanding.
Adding a new error type that should be handled means that consumers need to pay attention to the API docs and/or changelog. The compiler, linters, etc don't do anything to help you.
All of this culminates to an infuriating, inconsistent experience with error handling.
I don't agree. There isn't a standard convention for wrapping errors in Rust, like there is in Go with fmt.Errorf -- largely because ? is so widely-used (precisely because it is so easy to reach for).
The proof is in the pudding, though. In my experience, working across Go codebases in open source and in multiple closed-source organizations, errors are nearly universally wrapped and handled appropriately. The same is not true of Rust, where in my experience ? (and indeed even unwrap) reign supreme.
> There isn't a standard convention for wrapping errors in Rust
I have to say that's the first time I've heard someone say Rust doesn't have enough return types. Idiomatically, possible error conditions would be wrapped in a Result. `foo()?` is fantastic for the cases where you can't do anything about it, like you're trying to deserialize the user's passed-in config file and it's not valid JSON. What are you going to do there that's better than panicking? Or if you're starting up and can't connect to the configured database URL, there's probably not anything you can do beyond bombing out with a traceback... like `?` or `.unwrap()` does.
For everything else, there're the standard `if foo.is_ok()` or matching on `Ok(value)` idioms, when you want to catch the error and retry, or alert the user, or whatever.
But ? and .unwrap() are wonderful when you know that the thing could possibly fail, and it's out of your hands, so why wrap it in a bunch of boilerplate error handling code that doesn't tell the user much more than a traceback would?
One would still use `?` in rust regardless of adding context, so it would be strange for someone with rust experience to mention it.
As for the example you gave:
File::create("foo.txt")?;
If one added context, it would be
File::create("foo.txt").context("failed to create file")?;
This is using eyre or anyhow (common choices for adding free-form context).
If rolling your own error type, then
File::create("foo.txt").map_err(|e| format!("failed to create file: {e}"))?;
would match the Go code behavior. This would not be preferred though, as using eyre or anyhow or other error context libraries build convenient error context backtraces without needing to format things oneself. Here's what the example I gave above prints if the file is a directory:
Error:
0: failed to create file
1: Is a directory (os error 21)
Location:
src/main.rs:7
My experience aligns with this, although I often find the error being used for non-errors which is somewhat of an overcorrection, i.e. db drivers returning “NoRows” errors when no rows is a perfectly acceptable result of a query.
It’s odd that the .unwrap() hack caused a huge outage at Cloudflare, and my first reaction was “that couldn’t happen in Go haha” but… it definitely could, because you can just ignore returned values.
But for some reason most people don’t. It’s like the syntax conveys its intent clearly: Handle your damn errors.
yeah but which is faster and easier for a person to look at and understand. Go's intentionally verbose so that more complicated things are easier to understand.
What's the "?" doing? Why doesn't it compile without it? It's there to shortcut using match and handling errors and using unwrap, which makes sense if you know Rust, but the verbosity of go is its strength, not a weakness. My belief is that it makes things easier to reason about outside of the trivial example here.
If you reject the concept of a 'return on error-variant else unwrap' operator, that's fine, I guess. But I don't think most people get especially hung up on that.
> What's the "?" doing? Why doesn't it compile without it?
I don't understand this line of thought at all. "You have to learn the language's syntax to understand it!"...and so what? All programming language syntax needs to be learned to be understood. I for one was certainly not born with C-style syntax rattling around in my brain.
To me, a lot of the discussion about learning/using Rust has always sounded like the consternation of some monolingual English speakers when trying to learn other languages, right down to the "what is this hideous sorcery mark that I have to use to express myself correctly" complaints about things like diacritics.
I don't really see it as any more or less verbose.
If I return Result<T, E> from a function in Rust I have to provide an exhaustive match of all the cases, unless I use `.unwrap()` to get the success value (or panic), or use the `?` operator to return the error value (possibly converting it with an implementation of `std::From`).
No more verbose than Go, from the consumer side. Though, a big difference is that match/if/etc are expressions and I can assign results from them, so it would look more like
let a = match do_thing(&foo) {
Ok(res) => res,
Err(e) => return e
}
instead of:
a, err := do_thing(foo)
if err != nil {
return err // (or wrap it with fmt.Errorf and continue the madness
// of stringly-typed errors, unless you want to write custom
// Error types which now is more verbose and less safe than Rust).
}
I use Go on a regular basis, error handling works, but quite frankly it's one of the weakest parts of the language. Would I say I appreciate the more explicit handling from both it and Rust? Sure, unchecked exceptions and constant stack unwinding to report recoverable errors wasn't a good idea. But you're not going to have me singing Go's praise when others have done it better.
Do not get me started on actually handling errors in Go, either. errors.As() is a terrible API to work around the lack of pattern matching in Go, and the extra local variables you need to declare to use it just add line noise.
is even more succinct, and the exception thrown on failure will not only contain the reason, but the filename and the whole backtrace to the line where the error occurred.
But no context, so in the real world you need to write:
try:
f = open('foo.txt', 'w')
except Exception as e:
raise NecessaryContext("important information") from e
Else your callers are in for a nightmare of a time trying to figure out why an exception was thrown and what to do with it. Worse, you risk leaking implementation details that the caller comes to depend on which will also make your own life miserable in the future.
How is a stack trace with line numbers and a message for the exception it self not enough information for why an exception was thrown?
The exceptions from something like open are always pretty clear. Like, the files not found, and here is the exact line of code and the entire call stack. what else do you want to know to debug?
It's enough information if you are happy to have a fragile API, but why would you purposefully make life difficult not only for yourself, but the developers who have their code break every time you decide to change something that should only be an internal implementation detail?
Look, if you're just writing a script that doesn't care about failure — where when something goes wrong you can exit and let the end user deal with whatever the fault was, you don't have to worry about this. But Go is quite explicitly intended to be a systems language, not a scripting language. That shit doesn't fly in systems.
While you can, of course, write systems in Python, it is intended to be a scripting language, so I understand where you are coming from thinking in terms of scripts, but it doesn't exactly fit the rest of the discussion that is about systems.
That makes even less sense becasue go errors provide even less info other then a chain of messages. They might as well be lists of strings. You can maybe reassbmle a call stack your self if all of the error handlers are vigalente about wrapping
> That makes even less sense becasue go errors provide even less info other then a chain of messages.
That doesn't make sense. Go errors provide exactly whatever information is relevant to the error. The error type is an interface for good reason. The only limiting bound on the information that can be provided is by what the computer can hold at the hardware level.
> They might as well be lists of strings.
If a string is all your error is, you're doing something horribly wrong.
Or, at very least, are trying to shoehorn Go into scripting tasks, of which it is not ideally suited for. That's what Python is for! Python was decidedly intended for scripting. Different tools for different jobs.
Go was never designed to be a scripting language. But should you, for some odd reason, find a reason to use in that that capacity, you should at least being using its exception handlers (panic/recover) to find some semblance of scripting sensibility. The features are there to use.
Which does seem to be the source of your confusion. You still seem hung up on thinking that we're talking about scripting. But clearly that's not true. Like before, if we were, we'd be looking at using Go's exception handlers like a scripting language, not the patterns it uses for systems. These are very different types of software with very different needs. You cannot reasonably conflate them.
Chill with being condescending if you want a discussion.
The error type in go is literally just a string
type error interface {
Error() string
}
That's the whole thing.
So i dont know what your talking about then.
The wrapped error is a list of error types. Which all include a string for display. Displaying an error is how you get that information to the user.
If you implement your own error, and check it with some runtime type assertion, you have the same problem you described in python. Its a runtime check, the API your relying on in whatever library can change the error returned and your code won't work anymore. The same fragile situation you say exists in python. Now you have even less information, theres no caller info.
No, like I said before, it's literally an interface. Hell, your next line even proves it. If it were a string, it would be defined as:
type error string
But as you've pointed out yourself, that's not its definition at all.
> So i dont know what your talking about then.
I guess that's what happens when you don't even have a basic understanding of programming. Errors are intended to be complex types; to capture all the relevant information that pertains to the error. https://go.dev/play/p/MhQY_6eT1Ir At very least a sentinel value. If your error is just a string, you're doing something horribly wrong — or, charitably, trying to shoehorn Go into scripting tasks. But in that case you'd use Go's exception handlers, which bundles the stack trace and all alongside the string, so... However, if your workload is script in nature, why not just use Python? That's what it was designed for. Different tools for different jobs.
They should have made the point about knowing where errors will happen.
The cherry on top is that you always have a place to add context, but it's not the main point.
In the Python example, anything can fail anywhere. Exceptions can be thrown from deep inside libraries inside libraries and there's no good way to write code that exhaustively handles errors ahead of time. Instead you get whack-a-mole at runtime.
In Go, at least you know where things will fail. It's the poor man's impl of error enumeration, but you at least have it. The error that lib.foo() returned might be the dumbest error in the world (it's the string "oops") but you know lib.foo() would error, and that's more information you have ahead of time than in Python.
In Rust or, idk, Elm, you can do something even better and unify all downstream errors into an exhaustive AGDT like RequestError = NetworkError(A | B | C) | StreamError(D | E) | ParseError(F | G) | FooError, where ABCDEFG are themselves downstream error types from underlying libraries/fns that the request function calls.
Now the callsite of `let result = request("example.com")` can have perfect foresight into all failures.
I don't disagree that exceptions in python aren't perfect and rust is probably closest of them all to getting it right (though still could be improved). I'm just saying stack traces with exceptions provide a lot of useful debugging info. IMO they're more useful then the trail of wrapped error strings in go.
exceptions vs returned errors i think is a different discussion then what im getting at here.
I disagree, adding context to errors provide exactly what is needed to debug the issue. If you don't have enough context it's your fault, and context will contain more useful info than a stack trace (like the user id which triggered the issue, or whatever is needed).
Stack traces are reserved for crashes where you didn't handle the issue properly, so you get technical info of what broke and where, but no info on what happened and why it did fail like it did.
It's one piece of information, but logging at the error location does that still. And if you have a function that's called in multiple places how do you know the path that got you into that place. If it wasn't useful we wouldn't try to recreate them with wrapped errors
You wrap errors primarily to avoid the implementation detail leak. Even where errors have stack traces, you still need to do that, as was already described earlier. What debugging advantage comes with that is merely an aded bonus (A really nice bonus, to be sure. Attaching stack traces is computationally wasteful, so it is a win to not have to include them).
You can get away with not doing that when cowboy coding scripts. Python was designed to be a scripting language, so it is understandable that in Python you don't often need to worry about it. But Go isn't a scripting language. It was quite explicitly created to be a systems language. Scripts and systems are very different types of software with very different needs and requirements. If you are stuck thinking in terms of what is appropriate for scripting, you're straight up not participating in the same thread.
> I'm just saying stack traces with exceptions provide a lot of useful debugging info.
The Go team actually did a study on exactly that; including stack traces with errors. Like you, they initially thought it would be useful (hence the study), but in the end, when the data was in, they discovered nobody ever actually used them. Meaningful errors proved to be far more useful.
Science demands replication, so if your study disagrees, let's see it. But in the absence of that, the Go study is the best we've got and it completely contradicts what you are telling us. Making random claims up on the spot based on arbitrary feelings isn't indicative of anything.
That said, I think we can all agree there is a limited place for that type of thing (although in that place you shouldn't use Go at all — there are many other languages much better suited to that type of problem space), but in that place if you had to use Go for some strange reason you'd use panic and recover which already includes the stack trace for you. The functionality is already there exactly as you desire when you do need to bend Go beyond what it is intended for.
We were taught not to use exceptions for control flow, and reading a file which does not exist is a pretty normal thing to handle in code flow, rather than exceptions.
That simple example in Python is missing all the other stuff you have to put around it. Go would have another error check, but I get to decide, at that point in the execution, how I want to handle it in this context
It's not "common". You have to deal with StopIteration only when you write an iterator with the low-level API, which is maybe once in the career time for most of developers.
The point is that the use of exceptions is built into the language, so, for example, if you write "for something in somegeneratorfunction():" then somegeneratorfunction will signal to the for loop that it is finished by raising this exception.
I’d say it’s more common for iterator-based loops to run to completion than to hit a `break` statement. The `StopIteration` exception is how the iterator signals that completion.
> the exception thrown on failure will not only contain the reason, but the filename and the whole backtrace to the line where the error occurred.
... with no other context whatsoever, so you can't glean any information about the call stack that led to the exception.
Exceptions are really a whole different kettle of fish (and in my opinion are just strictly worse than even the worst errors-as-values implementations).
Your Go example included zero information that Python wouldn't give you out-of-the-box. And FWIW, since this is "Go vs Rust vs Zig," both Rust and Zig allow for much more elegant handling than Go, while similarly forcing you to make sure your call succeeded before continuing.
And also nothing about that code tells you it can throw such an exception. How exciting! Just what I want the reason for getting woken up at 3am due to prod outage to be.
I also like about Go that you can immediately see where the potential problem areas are in a page of code. Sure it's more verbose but I prefer the language that makes things obvious.
I also prefer Rust's enums and match statements for error handling, but think that their general-case "ergonomic" error handling patterns --- the "?" thing in particular --- actually make things worse. I was glad when Go killed the trial balloon for a similar error handling shorthand. The good Rust error handling is actually wordier than Go's.
I'm pretty familiar with the idiom here and I don't find error/result mapping fluent-style patterns all that easy to read or write. My experience is basically that you sort of come to understand "this goo at the end of the expression is just coercing the return value into whatever alternate goo the function signature dictates it needs", which is not at all the same thing as careful error handling.
Again: I think Rust as a language gets this right, better than Go does, but if I had to rank, it'd be (1) Rust explicit enum/match style, (2) Go's explicit noisy returns, (3) Rust terse error propagation style.
Basically, I think Rust idiom has been somewhat victimized by a culture of error golfing (and its attendant error handling crates).
> you sort of come to understand "this goo at the end of the expression is just coercing the return value into whatever alternate goo the function signature dictates it needs", which is not at all the same thing as careful error handling.
I think the problem is Rust does a great job at providing the basic mechanics of errors, but then stops a bit short.
First, I didn't realize until relatively recently that any `String` can be coerced easily into a `Box<dyn Error + Send + Sync>` (which should have a type alias in stdlib lol) using `?`, so if all you need is strings for your users, it is pretty simple to adorn or replace any error with a string before returning.
Second, Rust's incomplete error handling is why I made my crate, `uni_error`, so you can essentially take any Result/Error/Option and just add string context and be done with it. I believe `anyhow` can mostly do the same.
I do sorta like Go's error wrapping, but I think with either anyhow or my crate you are quickly back in a better situation as you gain compile time parameter checking in your error messages.
I agree Rust has over complicated error handling and I don't think `thiserror` and `anyhow` with their libraries vs applications distinction makes a lot of sense. I find my programs (typically API servers) need the the equivalent of `anyhow` + `thiserror` (hence why I wrote `uni_error` - still new and experimental, and evolving).
An example of error handling with `uni_error`:
use uni_error::*;
fn do_something() -> SimpleResult<Vec<u8>> {
std::fs::read("/tmp/nonexist")
.context("Oops... I wanted this to work!")
}
fn main() {
println!("{}", do_something().unwrap_err());
}
Right, for error handling, I'd rather have Rust's bones to build on than Go's. I prefer Go to Rust --- I would use Go in preference to Rust basically any time I could get away with it (acknowledging that I could not get away with it if I was building a browser or an LKM). But this part of Rust's type system is meaningfully better than Go's.
Which is why it's weird to me that the error handling culture of Rust seems to steer so directly towards where Go tries to get to!
Interesting. It is semi-rare that I meet someone who knows both Rust and Go and prefers Go. Is it the velocity you get from coding in it?
I have a love/hate relationship with Go. I like that it lets me code ideas very fast, but my resulting product just feels brittle. In Rust I feel like my code is rock solid (with the exception of logic, which needs as much testing as any other lang) often without even testing, just by the comfort I get from lack of nil, pattern matching, etc.
I think this is kind of a telling observation, because the advantage to working in Go over Rust is not subtle: Go has full automatic memory management and Rust doesn't. Rust is safe, like Go is, but Rust isn't as automatic. Building anything in Rust requires me to make a series of decisions that Go doesn't ask me to make. Sometimes being able to make those decisions is useful, but usually it is not.
The joke I like to snark about in these kinds of comparisons is that I actually like computer science, and I like to be able to lay out a tree structure when it makes sense to do so, without consulting a very large book premised on how hard it is to write a doubly-linked list in Rust. The fun thing is landing that snark and seeing people respond "well, you shouldn't be freelancing your own mutable tree structures, it should be hard to work with trees", from people who apparently have no conception of a tree walk other than as a keyed lookup table implementation.
But, like, there are compensating niceties to writing things like compilers in Rust! Enums and match are really nice there too. Not so nice that I'd give up automated memory management to get them. But nice!
I'm an ex-C++/C programmer (I dropped out of C++ around the time Alexandrescu style was coming into vogue), if my background helps any.
> Go has full automatic memory management and Rust doesn't
It doesn't? In Go, I allocate (new/make or implicit), never free. In Rust, I allocate (Box/Arc/Rc/String), never free. I'm not sure I see the difference (other than allocation is always more explicit in Rust, but I don't see that as a downside). Or are you just talking about how Go is 100% implicit on stack vs heap allocation?
> Sometimes being able to make those decisions is useful, but usually it is not.
Rust makes you think about ownership. I generally like the "feeling" this gives me, but I will agree it is often not necessary and "just works" in GC langs.
> I actually like computer science, and I like to be able to lay out a tree structure when it makes sense to do so, without consulting a very large book premised on how hard it is to write a doubly-linked list in Rust. The fun thing is landing that snark and seeing people respond "well, you shouldn't be freelancing your own mutable tree structures, it should be hard to work with trees", from people who apparently have no conception of a tree walk other than as a keyed lookup table implementation.
I LOVE computer science. I do trees quite often, and they aren't difficult to do in Rust, even doubly linked, but you just have to use indirection. I don't get why everyone thinks they need to do them with pointers, you don't.
Compared to something like Java/C# or anything with a bump allocator this would actually be slower, as Rust uses malloc/free, but Go suffers from the same achilles heel here (see any tree benchmark). In Rust, I might reach for Bumpalo to build the tree in a single allocation (an arena crate), but only if I needed that last ounce of speed.
If you need to edit your tree, you would also want the nodes wrapped in a `RefCell`.
I feel like this misses the biggest advantage of Result in rust. You must do something with it. Even if you want to ignore the error with unwrap() what you're really saying is "panic on errors".
But in go you can just _err and never touch it.
Also while not part of std::Result you can use things like anyhow or error_context to add context before returning if theres an error.
You can do that in Rust too. This code doesn't warn:
let _ = File::create("foo.txt");
(though if you want code that uses the File struct returned from the happy path of File::create, you can't do that without writing code that deals somehow with the possibility of the create() call failing, whether it is a panic, propagating the error upwards, or actual error handling code. Still, if you're just calling create() for side effects, ignoring the error is this easy.)
Which can also be said about Rust and anyhow/thiserror. You won't see any decent project that don't use them, the language requires additional tooling for errors as well.
Rust used to not have operator?, and then A LOT of complaints have been "we don't care, just let us pass errors up quickly"
"good luck debugging" just as easily happens simply by "if err!=nil return nil,err" boilerplate that's everywhere in Golang - but now it's annoying and takes up viewspace
It's just as easy to add context to errors in Rust and plenty of Go programmers just return err without adding any context. Even when Go programmers add context it's usually stringly typed garbage. It's also far easier for Go programmers to ignore errors completely. I've used both extensively and error handling is much, much better in Rust.
You could have done that in Rust but you wouldn't, because the allure of just typing a single character of
?
is too strong.
The UX is terrible — the path of least resistance is that of laziness. You should be forced to provide an error message, i.e.
?("failed to create file: {e}")
should be the only valid form.
In Go, for one reason or another, it's standard to provide error context; it's not typical at all to just return a bare `err` — it's frowned upon and unidiomatic.
Also having explicit error handling is useful because it makes transparent the possibility of not getting the value (which is common in pure functional languages). With that said I have a Go project outside of work and it is very verbose. I decided to use it for performance as a new version of the project that mostly used bash scripts and was getting away too cryptic. The logic is easier to follow and more robust in the business domain but way more lines of code.
What is the context that the Go code adds here?
When File::create or os.Create fails the errors they return already contain the information what and why something failed.
So what information does "failed to create file: " add?
The error from Rust's File::create basically only contains the errno result. So it's eg. "permission denied" vs "failed to create file: permission denied".
Whatever context you deem appropriate at the time of writing that message. Don't overfocus on the example. It could be the request ID, the customer's name — anything that's relevant to that particular call.
Well if there is useful context Rust let's you add it.
You can easily wrap the io error in something specific to your application or just use anyhow with .context("...")?
which is what most people do in application code.
"Context" here is just a string. Debugging means grepping that string in the codebase, and praying that it's unique. You can only come up with so many unique messages along a stack.
You are also not forced to add context. Hell, you can easily leave errors unhandled, without compiler errors nor warnings, which even linters won't pick up, due to the asinine variable syntax rules.
I'm not impressed by the careless tossing around of the word "easily" in this thread.
It's quite ridiculous that you're claiming errors can be easily left unhandled while referring to what, a single unfortunate pattern of code that will only realistically happen due to copy-pasting and gets you code that looks obviously wrong? Sigh.
"Easily" doesn't mean "it happens all the time" in this context (e.g. PHP, at least in the olden days).
"Easily" here means that WHEN it happens, it is not usually obvious. That is my experience as a daily go user. It's not the result of copy-pasting, it's just the result of editing code. Real-life code is not a beautiful succession of `op1, op2, op3...`. You have conditions in between, you have for loops that you don't want to exit in some cases (but aggregate errors), you have times where handling an error means not returning it but doing something else, you have retries...
I don't use rust at work, but enough in hobby/OSS work to say that when an error is not handled, it sticks out much more. To get back on topic of succinctness: you can obviously swallow errors in rust, but then you need to be juggling error vars, so this immediately catches the eye. In go, you are juggling error vars all the time, so you need to sift through the whole thing every goddamn time.
> Debugging means grepping that string in the codebase, and praying that it's unique.
This really isn't an issue in practice. The only case where an error wouldn't uniquely identify its call stack is if you were to use the exact same context string within the same function (and also your callees did the same). I've never encountered such a case.
> You are also not forced to add context
Yes, but in my experience Go devs do. Probably because they're having to go to the effort of typing `if err != nil` anyway, and frankly Go code with bare:
if err != nil {
return err
}
sticks out like a sore thumb to any experienced Go dev.
> which even linters won't pick up, due to asinine variable syntax rules.
I have never encountered a case where errcheck failed to detect an unhandled error, but I'd be curious to hear an example.
Now all you have to do is get a Go programmer to write code like this:
if somethingElse {
err := baz()
log.Println(err)
}
Good luck!
As for your first example,
// if only err2 failed, returns nil!
Yes, that's an accurate description of what the code you wrote does. Like, what? Whatever point you're trying to make still hinges on somebody writing code like that, and nobody who writes Go would.
Now, can this result in bugs in real life? Sure, and it has. Is it a big deal to get a bug once in a blue moon due to this? No, not really.
> It seems the Go development team has a high bar for adding features to the language. The end result is a language that forces you to write a lot of boilerplate code to implement logic that could be more succinctly expressed in another language.
Being able to implement logic more succinctly is not always a good thing. Take error handling syntactic sugar for example. Consider these two snippets:
and: The first code is more succinct, but worse: there is no context added to the error (good luck debugging!).Sometimes, being forced to write code in a verbose manner makes your code better.