Revisiting Go

A little over 6 months ago I wrote a post about Go’s idiosyncratic error handling, but abandoned Go soon afterwards. I’ve always had a lingering feeling that I hadn’t given it a fair shake, and so I decided that the release of Go 1.0 was as good a reason as any to give it another try. After hacking away at a simple server for a week or so I’ve found a few things to like, but a lot that leaves me profoundly unsatisfied.

So what’s to like?

I’ve always characterised Go as “C with garbage collection and closures”, and I still can’t really fault that description. Even when using goroutines and channels, it still feels like C. Possibly a C/Python hybrid. It’s very lightweight, it’s quite expressive and doesn’t overburden you with ways to get stuff wrong. You can be hacking away for a couple of days and feel like you really know the language, as opposed to C++ which still occasionally surprises me after 15 years.

I like the lightweight object system that seems to just work, although that could be because I’ve always preferred composition to inheritance when using OO type systems.

Go’s interface mechanism looks to be the functional equivalent of Haskell’s typeclass system, and seems to be a nice way of abstracting things. The jury’s still out for me on the implicit satisfaction of interfaces (especially as I can’t find a good way to find out if a type satisfies an interface short of throwing it at the compiler and seeing if it breaks), but I agree that it could be a useful property of the language.

Slices are a nice borrowing from Python (et al), and Goroutines and channels are a very nice way of dealing with concurrent problems.

The defer mechanism is a nice way of cleaning up after yourself. I think I still prefer C++’s RAII system, but defer is at a very nice point on the safety-vs-convenience continuum.

And coming from a day job using C++, Go’s compile times are fantastic. Mind you, being blazingly fast compared to a C++ compiler shouldn’t really be a challenge to a modern language.

But what’s wrong with it?

The major thing that annoys me with Go is not so much a part of the language, but the “simplicity above all else” attitude expressed by the Go project as a whole.

Yes, simplicity is a good thing. But far too often “simplicity” seems like an excuse to be wheeled out whenever somebody asks for something useful but nontrivial. After a while it stops sounding like sage advice and begins to sound like a petulant child whining “But it’s haaaaaard!”. Trying to keep things simple is a laudable goal, but when you achieve it by pushing all the complexity you don’t want to deal with onto every other developer on the planet, it’s time to re-evaluate your design goals. Things should be as simple as possible, yes, but no simpler.

Error Handling

I’ve also revised my opinion of Go’s error handling since writing my last post on it. It’s horrible. After writing little more than 500 lines of Go, if I have to write

 if err != nil { return err } 

(or its moral equivalent) one more time I’m going to hurl my computer across the room. Seriously. Contrary to my previously expressed opinion, it is underpants-on-head insanity.

Now, I do agree that people use exceptions to signal unexceptional situations far too often (Python, I’m looking at you here…). But even so, that is no reason to force every single Go programmer to write if err != nil { return err } on every second line of their program. I actually find it hard to reason about any Go function of more than 6 lines, because at least 4 of them will be the same bloody if err != nil { return err } nicely masking all of the important stuff that’s going on. Error handling via exceptions certainly ain’t perfect, but it’s better than this.

The whole return code idea also makes it nearly impossible to chain method calls or do anything even remotely succinct. If you like anything even remotely approaching a point-free style then Go is not your language.

On a side note, I’m all for multiple return values, too – but why not expand the concept into generalised tuples? Oh, that’s right: simplicity of implementation.

Generics

I fail to understand how you can launch a modern, statically-typed language without support for generic types. Yes, I know it complicates the typesystem implementation, but you know what? I DON’T CARE. By refusing to deal with this complexity once in the compiler, the Go team has forced each and every go developer to deal with it individually and separately. This doesn’t sound like the best tradeoff in history.

And yes, I also know that you can simulate a lot of what you’d normally need generics for using interfaces. That’s great. Now explain to me how passing around an interface{} is conceptually any different to, or any more typesafe than, passing around a void* (or an Object reference in Java).

Consistency

Everything in Go is passed by value. Oh, except strings. And maps. Oh, and slices. Arrays are pass-by-value, though.

I’m sure there are compelling design reasons for all these these, but to the rest of us it’s just arbitrary and error-prone.

Other stuff

My only other major gripe is that they’ve left out the one thing that would make channels truly useful: Discriminated Unions. With a discriminated union you could use a channel as a rich, type-safe communication channel (much in the same way you’d use a mailbox in F#), but there aren’t any discriminated unions in Go, so you can’t. Why? “Simplicity of implementation” again.

You can simulate this sort of thing using using interfaces, though. But thanks to the whole implicity-satisfied-interface thing you still have no compile-time guarantee about the range of types you’ll have to deal with, because potentially any type can implement your interface. Which means that you’re back to type-assertion-and-hope-for-the-best.

I’d also like to see some notion of const correctness (à-la C++), but that’s a pretty minor niggle compared to the rest of this rant.

Conclusion

Writing all this makes me sad, because I really, really, really wanted to like Go. Much more than I wanted to like Scala. There are some lovely features in there, and there is something genuinely endearing about it’s lightweight, git-er-done style. But the more I use it, the more it feels like a squandered opportunity.

Maybe it feels better if you come at it directly from C, where its feature set would be overwhelmingly positive. Maybe I’m just too set in my ways to appreciate its novelty. Or maybe it just is a fundamentally a flawed design, and no amount of wishful thinking is going fix it.

I wish I liked it more than I do.

2 thoughts on “Revisiting Go

  1. “I actually find it hard to reason about any Go function of more than 6 lines, because at least 4 of them will be the same bloody if err != nil { return err } nicely masking all of the important stuff that’s going on.”

    Maybe that is the point? In my experience software runs good until there is an error or corner case. Go forcing you to actually look at the error and make a decision on how it should be handled is huge. I don’t know how many times I have seen code where error handling was clearly an after thought.

    To me, making sure the error cases are handled correctly is just as important as “all of the important stuff that’s going on.” if not more important.

    But that is just opinion.

Leave a comment