5 ms·
Could you give examples of problems you encountered with this approach? The only issue I can think of comes from ignoring the error. You get the same problem w
by jspdown 3y ago
Could you give examples of problems you encountered with this approach?
The only issue I can think of comes from ignoring the error. You get the same problem with Rust's Result btw. Static analysis helps in this regard, golangci-lint catches easily such mistakes.
In my experience, with production tooling, I never encountered what you described. Though, from a pure aesthetic point of view I prefer Rust's Result.
- red_admiral 3y agoIf you return an `Either[Error, T]` (to use Scala syntax) then in case of error you never have to construct a T and the caller can never read one by accident on the error path, whereas if you return a pair (T, error) then you can hit at least three cases: 1. T is an int that's sometimes legitimately zero, if you return (0, err) and the caller isn't careful they can look at the 0 without realising it's not valid. 2. T is a 'pointer' type so you return (null, error) and the caller tries to dereference the pointer anyway (one example I've seen is inserting a logging statement for debugging purposes just _before_ the error-check, along the style of `logger.debug("foo {t.name}")` where the braces interpolate stuff). 3. T lives in a strongly typed world where there's no convenient null value available (maybe your language distiniguishes between "definitely a T" and "either a T or null") so you somehow have to construct an instance of a T even though you're never going to use it and it'll most likely be invalid in some sense. That doesn't mean the golang approach is bad - especially not in golang itself - but you do need to know how to use it correctly. Then again, using monadic error handling in golang would probably be strictly inferior - it's not that you can't define one, but the language doesn't have the syntactic sugar to handle it like e.g. Scala does, and I find that being able to use that sugar to get some readability for the approach is the whole point of monadic error handling.
- usrnm 3y agoCallee: creates a complex object and does several steps of initialisation. Last step fails, so it returns a partially initialized object and an error. Caller: checks the error, which is not fatal, but expects to get a nil in this case. Ends up with a non-nil partially initialized garbage. Yes, someone screwed up. Yes, it's a bug. Doesn't change the fact that there are better error handling approaches that eliminate this kind of bugs completely
- nordsieck 3y agoYou're right that the language doesn't protect you here. One easy way to prevent these sorts of errors is to always return default literals with errors (in the same spirit of "value == var" from c to prevent accidental assignment).
- haileys 3y agoFor starters it relies on implicit zero values in the language. These are dangerous from a modelling perspective: if you have a 0 integer, or an empty string, you can't really be sure if that's a meaningful value or merely a default. This is a bigger problem than just error handling. It makes it all too easy for invalid data to slip into your system. Implicit zero values are a bad design decision that cause all sorts of other pain. Consider the behavior of sends and receives on nil channels [1]. This is a fundamentally nonsense operation, but Go's designers are forced to define a behavior because nil channels can exist thanks to implicit zero values. [1]: https://dave.cheney.net/2014/03/19/channel-axioms https://dave.cheney.net/2014/03/19/channel-axioms
- tialaramex 3y ago> You get the same problem with Rust's Result btw How? Take the easy example, "clowns" is a string that's supposed to be an small integer. How do I get this wrong in Rust? We have to say what we want to happen when it won't parse which isn't ignoring it. let clowns: u16 = clowns.parse(); // Won't compile If we say we believe this won't happen we can express that, but when it does happen the program panics... let clowns: u16 = clowns.parse().unwrap(); // Panics if clowns doesn't parse as a u16 We can say what we actually want to happen, but how is that the same problem? let clowns: u16 = clowns.parse().unwrap_or(1629); // OK it's 1629 if it won't parse Finally we can write the C-style "Nothing will go wrong" attitude using unsafe, but I don't see that as a problem, if your first instinct is to reach for unsafe Rust you're a bad programmer let clowns: u16 = unsafe { clowns.parse.unwrap_unchecked() }; // Mark Baum says "Boom".