6 ms·
> 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
by oncallthrow 10mo ago
> 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.
- howenterprisey 10mo agoYou can just as easily add context to the first example or skip the wrapping in the second.
- fragmede 10mo agoyeah 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.
- loeg 10mo agolet mut file = File::create("foo.txt").context("failed to create file")?; Of all the things I find hard to understand in Rust, this isn't one of them.
- snuxoll 10mo agoImportant to note that .context() is something from `anyhow`, not part of the stdlib.
- fragmede 10mo agoWhat'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.
- loeg 10mo agoThe original complaint was only about adding context: https://news.ycombinator.com/item?id=46154373 https://news.ycombinator.com/item?id=46154373 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.
- filleduchaos 10mo ago> 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.
- snuxoll 10mo agoI 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.
- snuxoll 10mo agoEspecially 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).
- never_inline 10mo agofmt.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.
- snuxoll 10mo agoI 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.
- oncallthrow 10mo agoI 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.
- bargainbin 10mo agoMy 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.
- mh2266 10mo agoI think the standard convention if you just want a stringly-typed error like Go is anyhow? And maybe not quite as standard, but thiserror if you don’t want a stringly-typed error?
- kstrauser 10mo ago> 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?
- tptacek 10mo agoI 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.
- nu11ptr 10mo agoNah, you just need to use `map_err` or apply a '.context' which I think anyhow can do (and my crate, `uni_error` certainly can otherwise).
- tptacek 10mo agoI'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).
- nu11ptr 10mo ago> 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()); } Ref: https://crates.io/crates/uni_error https://crates.io/crates/uni_error
- edflsafoiewq 10mo agoPython's f = open('foo.txt', 'w') 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.
- oncallthrow 10mo ago> 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).
- reissbaker 10mo agoYour 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.
- verdverm 10mo agoWe 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
- phibz 10mo agoI 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.
- oncallthrow 10mo agoAny sane Go team will be running errcheck, so I think this is a moot point.
- afavour 10mo agoI think it’s still worth pointing out that one language includes it as a feature and the other requires additional tooling.
- dlisboa 10mo agoWhich 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.
- Mawr 10mo agoNot exactly: - https://github.com/kubernetes/kubernetes/pull/132799/files https://github.com/kubernetes/kubernetes/pull/132799/files - https://github.com/kubernetes/kubernetes/pull/80700/files https://github.com/kubernetes/kubernetes/pull/80700/files - https://github.com/kubernetes/kubernetes/pull/27793/files https://github.com/kubernetes/kubernetes/pull/27793/files - https://github.com/kubernetes/kubernetes/pull/110879/files https://github.com/kubernetes/kubernetes/pull/110879/files - https://github.com/moby/moby/pull/10321/files https://github.com/moby/moby/pull/10321/files - https://github.com/cockroachdb/cockroach/pull/74743/files https://github.com/cockroachdb/cockroach/pull/74743/files Do we have linters that catch these?
- wging 10mo ago
- sethops1 10mo agoI 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.
- the_gipsy 10mo ago"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.
- oncallthrow 10mo ago> 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.
- the_gipsy 10mo agoThe go stdlib notoriously returns errors without wrapping. I think it has been shifting towards more wrapping more often, but still. err1 := foo() err2 := bar() if err1 != nil || err2 != nil { return err1 // if only err2 failed, returns nil! } ``` func process() error { err := foo() if err != nil { return err } if something { result, err := bar() // new err shadows outer err if err != nil { return err } use(result) } if somethingElse { err := baz() // another shadow log.Println(err) } return err // returns foo's err (nil), baz's error lost } ```
- Mawr 10mo agoNow 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.
- NooneAtAll3 10mo agoit'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
- oncallthrow 10mo ago> "if err!=nil return nil,err" boilerplate that's everywhere in Golang - but now it's annoying and takes up viewspace This isn't true in my experience. Most Go codebases I've worked in wrap their errors. If you don't believe me, go and take a look at some open-source Go projects.
- YmiYugy 10mo agoWhat 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?
- edflsafoiewq 10mo agoThe 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".
- Mawr 10mo agoWhatever 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.
- YmiYugy 10mo agoWell 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.
- frizlab 10mo agoSwift 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.
- dystopiandevel 10mo agoAlso 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.
- nu11ptr 10mo agoThat 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.
- Mawr 10mo agoYou 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.
- nu11ptr 10mo ago> You could have done that in Rust but you wouldn't, because the allure of just typing a single character of ? is too strong. You could have done that in Go but you wouldn't, because the allure of just typing two words return err is too strong. Quite literally the same thing and the only difference is bias and habit.
- tayo42 10mo agoI 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.
- hnaccount19293 10mo agoIt'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.
- deleted 10mo ago[deleted]