I’m in the middle of writing a Coord blog post about why we use Go for building HTTP servers, and it’s made me want to organize my thoughts a little more about some of the design principles of Go. Probably the most common complaint you’ll hear about Go is, “too much boilerplate”! In fact, Go is explicitly designed to embrace boilerplate! Why would anyone use a language like this?

Functional programming adherents in particular find this very hard to take. The whole idea of functional programming is that you should write your common code as functions that can work in a wide range of circumstances, so functional languages “should” have no boilerplate, since you can encapsulate any repeated code into a functional wrapper.

And yet, we’re using Go at Coord, and we’re quite happy with it. Are we drowning in boilerplate? If so, how do we handle it? My two cents on this issue follow.

A boiler, get it? (Photo by Nick Cooper on Unsplash)

Go is made for readers, not writers, of code

The fundamental insight that made Go tolerable for me is this: in any language, code is easier to write than it is to read. But, unlike most languages, Go tries as hard as possible to level the playing field.

Many language features are designed to abstract away common tasks and to reduce copy-and-paste coding. This is often how we evaluate programming languages: how quickly can you write factorial? Or reverse a list?

But if you’re working on a team, the important feature of a language isn’t writeability: it’s readability. And Go was designed from the ground up to be as readable a language as possible. This is why they haven’t added all of the features that are so glaringly missing: every additional language feature is subject to misuse. Not by you — you’d never do such a thing! — but who knows what your coworkers are capable of? Plus, every line of code is read many times, even if it’s only written once. So reading time keeps adding up, while the writing time is mostly paid for at the beginning.

You can certainly complain about the exact trade-off that the Go implementors have made (I do constantly) but the more I work with it, the more I appreciate the fundamental concept of designing a language to be read rather than written.

Below, I talk about some of the criticisms of Go and explain why, even though I share many of them to some extent, I prefer the tradeoffs that Go makes to those of most other languages.

How many lists do you actually sum?

One of the most common ways of showing how much Go sucks is by trying to do a simple task like summing a list:

func sum(x []int64) (out int64) {

for _, v := range x {

out += v

}

return

}

What people will point out here is that this function only works for lists of int64s. If you wanted to sum lists of other int or float types — or, God forbid, a fancier type like a complex number or 3-vector — you’d have to write this function all over again, or use run-time reflection which gets rid of any type guarantees.

I absolutely agree! I would say that the lack of generics is the most visible problem with Go. This directly contributes to many of its other major flaws, especially its lack of higher-order functions, such as map over lists.

On the other hand, how many times do you write code where you don’t know what you have a list of? For us, the answer is, not many. There are definitely a few cases, but the vast majority of our code operates over known types. This is probably because we’re application developers, not library authors — but I bet you are an application developer, too!

So even though this language decision offends my sensibilities to no end, as an engineering manager, I can’t decide against my team’s using Go for its lack of generics alone. If it’s the right language for other reasons, this alone won’t doom it.

Explicit error handling is good for you

The other big source of Go boilerplate is its error handling. Almost every function returns its real output, plus an error. This is something that functional programmers, for once, can’t complain too much about — it’s pretty much just a dressed up Maybe monad — but it does make your code much longer than it would be otherwise.

The thing is, what’s the alternative? Not exceptions, surely! While having

if err != nil {

return nil, err

}

every other line can be painful, this pain is as nothing to the pain of accidentally failing to check for an error in production code!

Now, there are probably language solutions you could come up with for this problem, but even if you did, they’d mostly succeed in hiding the error handling from the writers and readers of your source code. I’m not sure this is a great trade-off. After all, you can always turn your monitor vertically if there are too many lines of code for you to read.

I Miss Ternaries

Probably the thing that annoys me the most as a day-to-day Go programmer is having things that you can only do as a statement that, in other languages, could be an expression. This is bad because it makes function composition impossible, and composition is the main building block of functional programming. There are lots of times when function composition results in more reasonable code than turning the same set of steps into a procedural recipe as Go forces you to do.

Error handling and the lack of generics are two big causes of this, but the third is the lack of a conditional expression. For instance, say you want to call a function with a certain input value or a default if it’s not set or invalid (very common in our API handlers). In JavaScript I could just write do_something(isValid(arg) ? arg : default); but in Go I have to spread this over many lines:

if isValid(arg) {

do_something(arg)

} else {

do_something(default)

}

If do_something was nested in a more complicated expression, I’m even more out of luck. It’s true that ternaries can be abused (Python’s syntax makes them particularly terrible) but in this case, I find the alternatives even worse. Because Go doesn’t allow truth comparisons for non-boolean types — another decision that’s probably good on balance — you can’t even do the equivalent of Javascript’s do_something(arg || default) if you’re just worried about arg being nil . You can’t even necessarily write a function to get around this problem, because you often need to rely on the short-circuit behavior of the ternary operator.

This is why, while generics are certainly the most obvious missing Go feature, I think that a conditional operator is the most important.

Conclusion

As I hope is clear by now, I am just as irritated with Go’s commonly-cited faults as the next person, but I’m irritated mostly as a code writer. As a reader, I find Go code very understandable, by and large.

I encourage you to think about languages, as well as the design choices you make in the systems you build, as a reader and not just as a writer. Even if the trade-offs you get to are different than mine, I suspect you’ll end up happier over the long run.