I wish people would quit the evangelical crap like that because you can find random bad design patterns and pitfalls in any language. At the end of the day general purpose languages have to fit a large criteria of needs for a wide criteria of developers while evolving and maturing along the process. So there will always be instances where a decision seems right at the time but later turns out to be bad. And even if you take Apple's approach of breaking your language with each iteration (as they do with Swift) you still end up with lots of wasted developer time porting your code with each new release.
Honestly if you aren't a skilled enough programmer to navigate the nuances of any particular language then you really are no better than kids playing in drag-and-drop environments like Scratch.
Does this look like a design mistake or a bit of oversight? There is no excuse why one should have to do
result, err := Foo()
if err != nil {
...
}
over and over again in a language designed after 2000. The usual argument is that Go's simplistic design "reduces complexity", yet explaining something basic as why the error handling system doesn't behave the way one expects needs you to know how the compiler represents interface types.
The next decade will see Go adding in most or all the complexity real-world software asks for, without ever admitting that maybe it should've been supported from the beginning without the hacky workarounds.
> Honestly if you aren't a skilled enough programmer to navigate the nuances of any particular language then you really are no better than kids[...]
Even though the bit in italics is pretty much the opposite of the "Go/JS pitch", I'll bite.
Sure, learning to code at a high level means you need to take time to learn things (which is the opposite of the pitch). I just don't get why teaching yourself to "navigate the [brokenness]" of a language that was out-of-date the day it was released is preferable to learning to write in an expressive language that won't artificially handicap you or provide you with an arsenal of footguns.
In all the time you were casting to and from interface{}, you could be exploring and using powerful and practical new ideas that make it less likely you'll suffer for failing to check one of those "err"s.
> Does this look like a design mistake or a bit of oversight?
Honestly, I don't mind error handling in Go. I've used a wide variety of languages in production systems (I've lost count but it's more than a dozen) and I've found Go to be surprisingly good at giving detailed, context aware breakdowns of where issues arise and allowing me to easily handle them. Sure there are a thousand different ways to do this and Go picked the ugliest, but in spite of that I've found it to be surprisingly effective - even in the more complex projects I've written like murex (my alternative UNIX $SHELL) and the odd Linux FUSE file system I've written to scratch a particular itch.
In fact for as many complaints like yours I've read there are also as many seasoned developers complementing Go's error handling. So I really think that particular issue is more a matter of personal taste rather than poor language design.
However I'm _not_ going to defend the nil thing nor how interface{}'s are (ab)used as an alternative to generics. Those _are_ just bad design choices.
But for all of Go's sins, I still find myself more productive writing Go code than I have in any other language for a long time (probably since writing Pascal in Borland's Turbo Pascal back in the 90s). Hence why I defend Go. I can understand idealistic opinions about language design (Go is opinionated too after all) but frankly what really matters is a developers ability to get an idea into something executable. And I feel a lot of the complaints about Go really miss the point about how productive that language is to a great many people and without sacrificing too much control to be useful in a practical sense.
The error handling idiom in Go is one of its strongest properties. Forcing developers to accommodate the error path inline and at each callsite makes code resilient and robust. It would stand to benefit from a Maybe feature, but even as-is it's a huge net positive versus e.g. exceptions.
People who complain about it don't grok the Go ethos. That's fine, it's not for everyone.
I'll note that The Fine Article is about why one of those established idioms confuses newcomers. Even better, it bites people because the type of an interface needs to be nil for something to work. (Insert appropriate expression of astonishment here.)
Enforcing patterns hiding inadequacies in the implementation works less well than one would imagine, e.g. look at all the languages that Go sought to improve upon and the baggage of "design patterns", GoF-alikes, etc. that they brought with them.
result, err := Foo1()
if not err {
Panic(err)
}
result2, err2: = Foo2(result1)
if not err2 {
Panic(err2)
}
This might allow you to skip the nesting of blocks, but why would you do this when multiple languages exist where you don't have to thread around error messages everywhere? For instance,
result1 <- foo1
result2 <- foo2 result1
(Yes, that's Haskell syntax, but you can do things with a similar lack of pain/verbosity/error-prone-ness in many languages.)
I used to fight Go's error system by creating wrapper functions to mitigate the need for nested blocks - essentially trying to "Haskellify" the code a little (for want a _very_ crude description). With time I realised I was spending more time over thinking a solutions and pontificating instead of just writing code and handling errors. Since then I've given up fighting against the language idioms I personally disagreed with and as a result I've come to appreciate some of the benefits they came with but I previously had overlooked. Ok `err != nil` is about as ugly as it comes, but it's still effective in getting the job done.
At the end of the day it doesn't make any more sense to apply Haskell methodologies to Go than it does to complain that Haskell is missing some fundamental features of Go. They're distinctly different languages. But despite this I've noticed you spend a lot of time in various Go discussions on HN moaning that Go isn't more like Haskell.
I use Haskell examples because that's the language I'm most comfortable with, but, e.g. the Result type used in Rust is another example of how this can be done better.
Ergonomic error handling or generics/parametric polymorphism aren't "Haskell methodologies". Go is one of a very small number of languages that have been designed in the last decade and lack features like this.
The reason I participate in HN comment threads about Go is largely how entertaining I find comments strenuously rationalizing Go's inadequacies. In one (very recent but definitely memorable) case[1], I was told that
> You [should] first reconsider your need to make a generic data structure and evaluate on a case by case basis.
1. Your assumption is wrong, I definitely didn't mean panic.
2. I think the arrow means assignment in Haskell and you are just referring to monadic errors? To use them the same way proper error handling is done in Go, you would just have more nesting and multiple unwraps, which is marginally different than Go syntax (but definitely with more compiler checking.)
The arrow syntax in Go is used by channels.
Apologies. In any case, monadic errors in Haskell allow you to make "failing early" automatic. Even a simple use of optional types can make a difference. For instance, here you're failing with the same error every time, like here:
a, err := squareRoot(x)
if err { handle(err) }
b, err := log(a)
if err { handle(err) }
c, err := log(b)
if err { handle(err) }
you can just use an optional ("Maybe") type: write a, b, and c with types like
a :: Double -> Maybe Double
and then do
a <- squareRoot x
b <- log a
c <- log b
If the computation of a fails, the whole computation fails. The compiler takes care of all the error-checking plumbing. I think the ergonomics of this common kind of situation are really suboptimal in Go, which to my knowledge doesn't support anything remotely similar.
Stuff like null or nil is a problem that's been identified for decades, and there are relatively widely-used type system approaches that solve it very nicely (sum types). It's not okay in a modern programming language to repeat the mistakes of decades-old ones in the name of "simplicity".
Checking the value of a property [edit: return of method] after you've nil'ed the parent object is enough raise an exception in most languages. So yes I'd say that's an edge case. Where Go gets it wrong here is because nil isn't really `nil` you get a silent `false` rather than an obvious crash + stack trace. But regardless of the bad design of Go around the usage of "nil", the code would have failed in pretty much any other language anyway.
You're not "checking the value of a property after you've nil'd the parent object", you're checking if you were given a nil. This issue can occur for any function which takes an interface-typed parameter. That's usually how it happens: somebody passes in a `nil` which comes from a pointer-typed variable: https://play.golang.org/p/ADTvLDDrw6
> But regardless of the bad design of Go around the usage of "nil", the code would have failed in pretty much any other language anyway.
No, it would not. In Java, null is null whether it's typed as a concrete reference, as an array or as an interface.
> You're not "checking the value of a property after you've nil'd the parent object", you're checking if you were given a nil.
Sorry, it's a method not a property, but I think my point remains valid with regards to the example in that article. Just to be clear, I'm not trying to defend nil here, but I do think it's important to understand the issue because I think the authors code would have failed regardless of the language. So Let's break the code down: first they create a struct exposed via an interface{}
type T struct{}
func (t T) F() {}
type P interface {
F()
}
func newT() *T { return new(T) }
Then they create an initialized variable that object type:
t := newT()
t2 := t
...and set that interface{} to nil:
if !ENABLE_FEATURE {
t2 = nil
}
Then they check the value returned from a method of the struct - bare in mind this is after the struct has been `nil`ed:
If there's a likelihood that they could be working with nil interfaces then they should be first checking the value of the interface before checking the value of the methods within it. Most OOP languages would raise an exception / print runtime error (in the case of JIT dynamic languages) or downright crash if you tried to access methods or properties of a nil / null / whatever type. So I'm not defending Go's behavior but their example is peculiar to say the least.
That all said, I do feel your examples are a lot more relevant to this discussion than the one that prompted the discussion to begin with.
>Most OOP languages would raise an exception / print runtime error (in the case of JIT dynamic languages) or downright crash if you tried to access methods or properties of a nil / null / whatever type.
Most OO languages wont report an interface as non null if its value is null. Go will.
> Sorry, it's a method not a property, but I think my point remains valid with regards to the example in that article.
Not really, Go supports (and encourages properly handling) nil method receivers.
> Most OOP languages would raise an exception / print runtime error (in the case of JIT dynamic languages) or downright crash if you tried to access methods or properties of a nil / null / whatever type.
Setting aside the fact that this is not quite true[0], there is a gulf between "failing" as a clear runtime (or compile-time) error at the point of an incorrect invocation, and "failing" by silently yielding a nonsensical state and possibly but not necessarily faulting at some other point later on. PHP gets regularly and deservedly panned for the latter.
[0] nil is a message-sink in obj-c (any message can be sent to nil and will be a no-op returning nil), and you can make Ruby or Smalltalk behave that way (or any other) as their nil is a regular object with a normal type which you can go and extend
Honestly if you aren't a skilled enough programmer to navigate the nuances of any particular language then you really are no better than kids playing in drag-and-drop environments like Scratch.