> 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.
You can just as easily add context to the first example or skip the wrapping in the second.
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 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.
it's the other way around
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
Swift is great for that:
do {
let file = try FileManager.create(…)
} catch {
logger.error("Failed creating file", metadata: ["error": "\(error)"])
}
Note the try is not actual CPU exceptions, but mostly syntax sugar.You can opt-out of the error handling, but it’s frowned upon, and explicit:
let file = try? FileManager.create(…)
or let file = try! FileManager.create(…)
The former returning an optional file if there is an error, and the latter crashing in case of an error.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.
That isn't apples to apples.
In Rust I could have done (assuming `anyhow::Error` or `Box<dyn Error + Send + Sync>` return types, which are very typical):
let mut file = File::create("foo.txt")
.map_err(|e| format!("failed to create file: {e}")?;
Rust having the subtle benefit here of guaranteeing at compile type that the parameter to the string is not omitted.In Go I could have done (and is just as typical to do):
f, err := os.Create("filename.txt")
if err != nil {
return err
}
So Go no more forces you to do that than Rust does, and both can do the same thing.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?
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.
"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 feel like almost always `?` is a mistake in Rust and should just be used for quick test code like using unwrap.
Go's wrapping of errors is just a crappy exception stack trace with less information.
Python's
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.