Hacker Read top | best | new | newcomments | leaders | about | bookmarklet login

> I don’t think I’ve ever seen a case where a Go function returned neither a value nor an error, or both a value and an error.

That's kind of the point. The type system should be powerful enough to disallow those cases then.

In practice, I've seen both, always accidentally. I've also (more commonly) seen a lot of confusion and annoyance around:

Okay, so this has to return a pointer for the error case, should the caller check that? If not, how do we square that with checking for nil pointers being generally a pretty good rule? If we do check, our unit test coverage has a blemish for every call since nothing can hit that. If we skip it being a pointer, then it's a zombie object.

It's just a lot of cognitive load and bikeshedding around an issue that shouldn't exist.



sort by: page size:

> I’m not a go developer. How does go document how a function can fail?

There's no magic to it. Errors are values, so it's a part of the function signature that there's an error code to check. In C++ any function can throw an exception and there's no way of knowing that it wont.

It's true that go doesn't document what _kinds_ of errors it can throw, but at least I know there's something to check.


> But doesn't know how the return values will be used by the caller. What is perhaps lost in this is where Go says that values should always be useful?

It doesn't matter how it will be used by the caller. If I'm writing a function that can fail, no magic in existence can create a success object out of nothing, especially one that "should always be useful". At that point you're stuck either returning a nil pointer or a zombie object (along with the error).

> Exactly the current way is what is said to be deficient, though.

It's deficient because it's modelling the wrong (in the common case) thing. I'm saying if you're in the uncommon case and that actually _is_ what you're trying to model, then you still can.

> A function of this type is naturally going to return a file every time because a file is always useful, even when there is failure.

What? No. It's not useful if there is no file, if the error is "wtf, that file doesn't exist".


> I like Go. But how come they have not fix the issue with implicit nulls? Null guards everywhere... I cannot believe that a company as mighty as Google did not choose to use decades old insights on how to fix this

They relied on decades old insights of persons who worked on C and Unix. That said, I'm honestly still on the fence about if nil checking is really braindead, or just what it boils down to in the end and the rest like exceptions and Options/Result is just theatrical.

What I don't get is that they are not more pedantic about checking nil errs in the compiler. They error hard during compilation on unused variables, but err checking is simply not done.


> What does it have to do with errors?

The fact that the former scenario is much more likelier to happen in golang due to its error handling. The former can be caught using unit tests, and should stick out more in languages like Java with scoped try-with-resources blocks that limit the scope of lifetimed variables (another bad design in golang where defers are function scope, not local scope limited, introducing unnecessary runtime costs, while also being less useful in practice).

At the same time, you don't want to be writing unit tests for every simple scenario. Some people who use dynamic languages argue that you just type assert everything in unit tests and you should be good. Obviously, that's not how things work out in practice. Similarly here, because of the way errors are handled in golang, the same pattern is everywhere in the program, and it is extremely tedious to write the same unit tests over and over everywhere, while making sure to reach a certain level of coverage.

> Not all Go functions, even in the standard library, return errors declared using the error interface.

Do they just return integers then like C?

> Why would you ever need a stack trace when the network is down, for example?

Because it's still important to know where you are in the program when the network broke, in order to make sure that recovery happened correctly for example. Seeing a "network is down" error in the logs is useful, but more so is knowing exactly what I was doing (what if I wasn't expecting to access the network in a particular path? etc.)

> Every problem you encounter with errors will also be encountered with other types sooner or later.

Practicality matters, otherwise C is all we'd ever need, and we wouldn't be having constant CVE's etc.

> Mentioning Rust here is a is a bit strange seeing as how it contradicts the entire premise you're trying to push.

Not really. Rust errors compose nicely, and you're explicitly forced to handle them (unlike golang's), and are much harder to accidentally swallow or ignore compared to golang. This scenario is repeated many times when you compare golang to other better designed languages. The language constantly takes the easy way out so to speak, making the compiler implementation simpler, while pushing complexity onto the user. Furthermore, as I pointed out, there's nothing preventing us from using the same approach in Java, now that it has sealed types and pattern matching. This is not possible in golang.

Java's checked exceptions are not much different actually. They still require you to handle the error either by try/catch or by declaring that the method throws the same exception, or a superclass of it.

I also remembered another shortcoming of golang error handling that I've seen several times in real code bases, being forced to check both the error and the other return value to make sure that things are working. Yes, a properly written program shouldn't need to do that, but reality doesn't care. And what's ironic is that golang was supposed to be designed to support "programming in the large" (another unverified claim that is contradicted by reality). The fact that it opens these doors is indicative of the mentality that went into designing it.


>To me errors are very special constructs

Special how? Surely they are not special in a way that can't be captured by a type system. You say they should be handled in a special/deliberate way, but that's what it means for something to have a distinct type: it can't be handled like everything else. You just define special error-handling functions and they will work on errors and nothing else, and nothing else will work on errors.

As far as syntax goes, the I feel the big issue is about deferring error handling. You don't always want to mix error handling code with your algorithm logic, so you need to defer. We also talk about 'errors' so generally that we never bother to qualify which kinds of errors should be deferred or not in a way that is encapsulated in the type system. Or what kind of deferring should be available -- return errors or just move them later in the same scope? (Java's checked exceptions are a good example of attempting to do this, but it doesn't work out in practice. Programmers don't think like programs do: deferred error handling isn't the same as deferred error-handler writing, and both are needed in different ways.)

From what I can tell about error handling in Go, it isn't really a step forward. (And we really want a step forward.) Go just kind of falls back on "this was the last thing that worked, and making significant progress is probably too hard, so we won't bother trying."

That's the impression I get, at least. I don't actually do any Go programming, but I'm not inspired by its approach, either.


> so you simply can't forget to handle an error.

This is such an important point, and I know people may think its outrageous that this could happen, but I work with a bunch of talented programmers, and still we've had the occasional bug crop up in our Golang codebase where we have a `val, err = doSomething()` without an ensuing `if err != nil { }` check which was missed by the programmer and by the reviewer. Or even better, we've had `val, _ = doSomething()` occur, the result of debugging to get around the unused variable compilation error.

It's 2020. Just like nil-pointers, this is an issue that is for the most part _solved_ and shouldn't exist in modern languages. Our programming languages should be constructed with the fact that we're fallible humans in mind.


> Bad code needs to be screamingly obvious.

I'm not sure I understand why this is true for Go rather than a statically typed FP language. If anything, I'd consider languages like Haskell or ML to be better at this.

- The obvious case is handling `nil`. In Haskell, you'd treat this as `Maybe a`, and it's obvious that you're not handling the nil case since the pattern match is incomplete. In Go, you get a nil pointer exception at runtime. You could check for the nil case for every pointer, but that's too onerous, so now you're left with relying on the programmer on checking

- Go doesn't have exceptions, which removes some of the hiding of bad code (see https://devblogs.microsoft.com/oldnewthing/20050114-00/?p=36...), but you can still accidentally use a result when an error is return (in fact, you have to return something!). In Haskell, you're forced to pattern match on an `Either`, which again exposes bad through an incomplete pattern match.

I don't think these are arguments for FP per se, but I do think that statically typed FP languages (and the languages they inspire) are much better about removing footguns and making bad code hard to write, simply because the typechecker is so restrictive. As a more general principle, instead of making bad code screamingly obvious, make it impossible to write!

Now, you mention later in the thread that you've already used Haskell before, so you're probably aware of all this and I may misinterpreted what you meant. Did you mean a different form of bad code?


> The only thing in Go source code that you see more often than the boiler plate "if err != nil" is "a, _ = foo()".

Where did you see that ? because that's not been my experience at all, and I've looked at a lot of Go code.


> Is Go fundamentally type-unsafe?

Interface{} and nil pointers point to yes.


> This really irritated me when I started working with go, but it stopped bothering me and now I even mostly like it.

Don’t get me wrong I like a good unused code warning.

What frustrates me is that Go’s is dumb / unreliable, and it will stop you from working entirely until you’ve complied with this whim, which has a fraction of a percent chance of identifying a real bug.

> Basically writing go without `staticcheck`[1] is not recommended.

So why have these things as mandatory compiler errors?


> like having your IDE cover the other 100 some-odd places of boiler plate such as writing getters and setters, and implementing comparable and hashCode and every other piece of insanity?

What is insane is believing returning errors is actually handling them, it is not, you're just passing the burden on the caller.

Second, Go isn't free of all the bullhshit you described so I'm not sure what is your point, that you don't have to write getters and setters in Go? lol, off course you do have to.

> Also, I much prefer to handle my errors where they happen in the code rather than do Pokemon try/catching

Who cares, go back playing Pokemon since apparently that's the only thing you can think off and stop being an hypocrite. What do you think panics are? yes, it's an half-baked exception system, like everything in Go, a language designed by people who clearly didn't know what they are doing, for people who don't.

> Sorry, the if err != nil horse has been beaten to death already

Yes, and don't worry, people will keep on beating that horse, because it was designed by people that clearly don't know what they are doing.


> until you write

Yeah, you can definitely opt out of it and there are even cases where the compiler doesn't guarantee that you're handling the errors, but assuming you're making a good faith effort to adhere to conventions, then you're thinking about your errors.

> Besides, Go errors are used in pair with the return value, so you have to check 4 possible combinations of value/nil and error/nil instead of 2 due to the absence of sum types.

No, you don't. If the error is not nil, there's an error. Sum types would be nice, but they wouldn't reduce the number of checks, they'd just formalize the idiom.

> Oh, and no monadic chains/exception mechanism, so feel free to write

I'm fine with this. Monads are hard for people to understand, and I'm not playing code golf...

> Go crowd had just invented the worst and least error-prone one.

I don't think it's the least error-prone, but it's not bad.


> How can you know that half the things I can represent are wrong? There are many situations where both returning a value and not returning a value are both valid. And where errors conditions may not dictate what the returned value should be.

In cases where it's valid, you use a type that makes it valid. Much of the point of having a type system is that the return type tells you exactly what the valid things for the function to return are.

Having an explicit declaration of which functions might return both a result and an error and which functions will definitely return one or the other but not both makes your code easier to understand and work with. As it stands, I would bet that most Go programmers ignore the possibility of returning both a result and an error even in functions that do it deliberately, because the idiomatic thing a function with that signature does is return one or the other but not both.


> but I kinda like having errors as values

this is the sort of silliness that is common in the Go world. as noted elsewhere, errors are values in lots of languages - Rust, C, Scala, Haskell, etc etc, but Go explicitly has no way to handle them nicely, no specific syntax and no fancy type system stuff like sum types.

it is my very strong belief that this will eventually be fixed in Go and when it does, almost all the people currently saying "I like errors being values [and it's fine that Go makes it very annoying]" will quickly prefer having some actual language help for these values.


> I want them to be front and center, equal to everything else.

That’s fine, and that’s why the function itself will have a return type of `Result<T, E>` for some meaningful return type T and error type E.

Even better, if there’s an error, there is no non-error return value. You can’t accidentally use the zero-valued return half of a tuple (as you can in golang) because it simply isn’t there.

Is the important part of error handling having some copy-pasted stanza repeated everywhere? Or is it enforcing that errors are always handled and semantically-undefined return values are never accidentally passed along in the event of an error?

> But ultimately it's about explicitness, obviousness. `?` is easy to miss, and permits method chaining, the outcome of which is incredibly easy to mispredict.

No, it simply is not. `?` early-aborts the function and returns the result straight away if it’s an error, and unwraps the interior value if not. There is no plausible way for someone to mispredict this behavior, and if there was, it would be no different from golang, since the two constructs are semantically virtually identical. One is simply shorter than the other.

`?` is no less explicit than three lines of copy-pasted code and both its existence and behavior are forced due to the function’s return type.

> And in imperative code, which is the supermajority of all code, `?` gives no meaningful increase in speed-of-reading -- which is a bogus metric, anyway.

Ease of understandability is almost hands-down the most important metric given the ratio of frequency to code being read versus written. And to be completely blunt, it is flatly ridiculous that wrapping every line in nearly-identical error handling code somehow doesn’t impair comprehension. The argument is the same for abstractions like `map`, `select`, `reduce` et al. Intent and behavior of code can be understood at a glance when you remove the minutia of looping, bounds-checking, and indexing and focus on just the operation. And as an added bonus, you remove surface area for potential bugs like off-by-one or fencepost errors.

Having nearly identical error-handling everywhere both in theory and in practice obscures the places where something is different. It is hard to notice small differences in largely-identical blocks of visual information—hence the existence of “spot the difference" games—but it is trivial to spot when those differences are large.

I genuinely struggle to comprehend how people can have ideas like this when they fly in the face of what little hard evidence we do have about syntactic differences in programming.


> Why does their existence mean that the other 95% of functions that can error need be given the wrong return type and pretend to return a tuple when they never do?

They don't, do they? One really nice thing about Go is writing and calling functions that _don't_ return any error. You can be confident that they will not throw any exceptions at all.


> Literally the first non-trivial code I wrote in go… I knew exactly where and how things could fail and where things were failing just by looking at the code.

Could you give an example?

I think you’re talking about something different than what I’m understanding. One of the major frustrations I have with Go error handling is the lack of stack traces, which means I often have to modify code in order to find out where an error occurred.

I’m pretty sure that’s not what you’re talking about, though.


> I really dislike exceptions because there's no documentation for how a function can fail. For this reason I prefer go style errors, which are an improvement on the C error story. Yes it has warts, but it's 80% good enough.

I’m not a go developer. How does go document how a function can fail?

A Java developer can use checked exceptions so that some information is in the signature. For unchecked exceptions the documentation must explain.

I guess in Go the type of the error return value provides some information but the rest needs to be filled in by the documentation, just like the Java checked exceptions case.


> Does it matter that it’s possible to misuse errors in Go if it virtually never happens? I just don’t find the point about what is possible interesting.

If you knew for sure that it virtually never happened, maybe not. But you don't. At best you know that a few particular individuals you're familiar with never mess it up (but then again, consider the people who "virtually never" write incorrect C). You can't trust random packages you haven't vetted, and you certainly can't trust code written by your junior software engineers.

> The other trade offs around readability, ergonomics, and so on seem more impactful.

Sum types have significantly better ergonomics. `(Result | null, Error | null)` takes four branches to handle properly, whereas `Either Result Error` takes two. And of course things get much worse once you're more than one layer deep, which in a language as procedural as Go you almost always are.

next

Legal | privacy