9 Things I Like About Go

Jul 24 2014

Yesterday I found one of my first Go apps. In a moment of retrospection, I realized how happy I am working with the Go language. I thought I'd take a moment to document why.

Here, in no particular order, are the top 9 reasons I like working with Go:

1. The Toolchain

A C programmer was looking at one of my Go projects. He was sorta irked at me: "Where's your Makefile?" I had to explain to him several times that I didn't need one. The go tool handled all of that for me. "But what about dependencies?" Handled. "Installing?" Handled. "Generating documentation?" Handled.

Go feels like a language by developers for developers, and nowhere is this evidenced more than Go's fantastic tool chain. Simple and powerful, the go porcelain makes it dead simple to build, run, format, and debug code.

I like the obvious tools (go build, go test), but I really love the deeper thought and work that went into things like race detection, formatting code (which I'll come back to), vetting, and installing remote packages (go get).

2. Clean Code

Few developers take this important lesson to heart: When it comes to writing good code, the cleanliness and readability of it is just as important as the functionality. It's good for others who will be reading and editing your code, and it's good for future-you, who will curse past-you when the code is too difficult.

One of my colleagues recently made this remark about Go: "It doesn't seem to matter who writes the code, it all looks similar. I can read it." In my mind, that's one of the highest praises that a programming language as a whole can garner. Yes, sometimes Go is more verbose. Error handling is even intentionally so. But verbosity for the sake of clarity is a noble exchange, and many coding crimes have been perpetrated in the name of brevity.

The fact that coding conventions have been established from the beginning, and are gently enforced by go fmt (and golint) contributes to the cleanliness of Go code. But so does the simple syntax and the absence of convenient but often confusingly misused features like ternary operators.

3. Go Routines

We recently ported a major application server from Java to Go. And one of the reasons is that writing good multi-threaded Java code is hard. It's not elegant, it requires a lot of conceptual overhead, and it consumes a lot of resources.

In fact, most of the major languages I've worked with make parallel computing a chore. It used to be the case that when I heard "we need to make it multi- threaded", my brain automatically said, "Add two weeks to the development schedule."

But Go is not like that. Go routines are conceptually simple, functional, and easy on the resources. (We once accidentally generated a few hundred thousand of these on an Amazon small instance!) In fact, working with Go routines is so easy that I now use them by default! I don't even have to think about how I will manage resources or clean up the mess... I merely have to ask whether it makes sense to run in parallel. That's it!

When it's this easy to make the right choice, something was clearly done right.

4. Channels

Hot on the heels of any discussion of go routines, we have to talk about channels. In a nutshell, channels provide a communication layer between go routines. And they work sorta like type-sensitive sockets.

I had about a week to re-implement (on a small scale) a proprietary message queueing system provided to us by a third party. The replacement I wrote had to be compatible with all of their existing libraries, and it also had to be robust enough to handle the real time queueing and relaying.

Channels are such a natural fit for this problem that a core of a few hundred lines handled all of the fanning in and out necessary for the main message queue. With a quick API wrap-around, I had the server done with time to spare. And it was faster than the original solution.

5. Metaphysical Parsimony

I have a substantial background in philosophy. One thing that always fascinates me is reading the different approaches philosophers have taken in the field known as speculative metaphysics. Roughly speaking, this is an exercise in world-making.

In the 17-19th centuries, speculative metaphysics reached something of a pinnacle. Descartes, Spinoza, Leibniz, Hume, Kant, Hegel, and many others devised systems to describe the essential components of reality. Yet if you were to sit down and read metaphysics, one book after another, you'd be shocked at how different each metaphysician's outlook is from the others. To read, say, Hegel compared to Descartes, one would thing two entirely different universes were described.

Programming languages are like that, too. In fact, a programming language is a metaphysical system. It describes what sorts of "realities" you can build. It describes how things interact in that "reality". In fact, it even defines what a "thing" is in that world.

The trouble is, many programming languages get so bogged down in metaphysical purity that it becomes a chore to build things.

Go's approach to the metaphysics of programming is marked by a tendency toward the austere. But not austerity or for the sake of ideological purity. Rather, it feels as if Go was written as an answer to the question "what do we actually need in order to write well-ordered and elegant software?"

The result: Building Go applications is a practical exercise, not an ideological one.

6. Standard Libraries for Today

Go is what I would call immanently useful. That means it has features that make it practical for writing code without needing a ton of supporting libraries.

I was a teenager when I started coding in the mid-1990's. And Java drew my attention because it was "not my parents' language." Hot with Internet-oriented technologies, I could build things with the JDK that would have required dozens of supporting libraries in other languages.

Today I feel the same way about Go that I did about Java in the '90's. It's a language that's captured the Zeitgeist. Rich with network-focused and web-focused libraries, Go provides an out-of-the-box toolset that is conducive to today's cloud-centric applications.

To wit, Go has a template engine, fantastic network and HTTP services, crypto support, a built-in AST parser, and a logging system.

My Java example should come as a warning, though: 20 years from now, Go may have that same dated feel that Java has today. But should that put us off from using it now? I don't see why it should.

7. Core Data Structures

Go doesn't have generics. Go probably won't have built-in generics. Rob Pike has made this clear repeatedly. Understandably, many developers feel like one of Go's big deficiencies is its lack of generics-backed collections.

But I would like to point out the contrary view. Go has the right number of built-in collections. It has first-class support for arrays and maps, along with built-in support for slices, an abstraction that feels like growable arrays.

These data structures are what we might call essentials. No moderately sized and well structured application can be built without (roughly) these sorts of collections.

"But it should have linked lists and binary trees and skiplists and stacks and queues... and these should either be built-in or allow generics." I hear that complaint, and on any given day I might even grouch that way myself. But when I get really honest, I admit that I don't really use most of those data structures (in their pure form) that often. And when I do, I need it for a specific purpose, and implementing it is not a big deal.

(Note: Go does have some built-in data structures like linked lists. I rarely use them.)

For a language clearly trying to reduce cognitive overhead, I think Go has made the admirable trade off: Deep and thorough support for a few fundamental data structures.

What about generics? I'm afraid that I just can't get too excited one way or another about them. I've seen grave crimes committed with generics systems, and a big part of me is both happy and relieved that I won't ever witness those in Go. On the reverse side, there are clearly cases where I'd rather have generics than worry about, say, someone inserting the wrong object into my []interface{}. (Easy solution: Don't use []interface{})

8. Multiple Returns (and Error Handling)

Go functions can return multiple values. The first time I saw Go, I said to the person standing next to me: "Multiple returns? Are you kidding? That's the dumbest thing I've ever seen!"

I'll gladly eat those words.

First, multiple return values make good programming even easier. When we wrote Cookoo, one of the functions we wrote has a signature like this:

func Cookoo() (*cookoo.Registry, *cookoo.Router, cookoo.Context)

Yes, that's right... it's one function that takes no parameters and returns three things. The reason I'm proud of this function is that it has hidden a ton of boilerplate setup from the user. All three of these objects are related to each other, and we can encapsulate all the work of relating them without the library user having to do anything. By the time they get all three of these, they don't even have to be aware of the relationship. Devs can just get to work.

But the prevalent usage of multiple returns is the error handling pattern pervasive throughout Go:

func DoSomething() (Result, error)

By convention, Go functions return their errors as the last return value. And this return value is declared as an error type.

  • It is easy to detect when an error happened: The error will be non-nil.
  • Errors are handled in the main flow of the program, not in additional special structures like try/catch.
  • The caller still has the opportunity to return a meaningful value along with the error (a practice I'd love to see become universal). This makes it easier for a program to recover.

New Go developers occasionally complain that this method of error handling makes their code longer. Does it really? Surprisingly, no. Rather, it encourages developers to handle errors right away. It looks more verbose because it avoids the Java-esque (though by no means restricted to Java) anti-pattern of passing exceptions back up the execution chain until some default handler somewhere finally catches.

9. Interfaces

I am learning a lot about good architecture simply by writing Go programs. The implicit interfacing in Go is a great teacher.

Go uses interfaces to represent functional capabilities of disparate types. The typical example is Go's io.Reader interface. The interface looks like this:

type Reader interface {
    Read(p []byte) (n int, err error)
}

Simply stated, to be a Reader is to have a Read function (matching the signature above).

Since interfaces are implicit, an implementation of the interface needs no declaration. It simply needs to provide the Read function and it is, in virtue of that, an io.Reader.

This mechanism is profoundly powerful. First, it accomplishes the same outcome as explicit interfaces do in other languages (like Java). Second, it encourages careful thinking about the topology of our code. In other words, it helps us design systems with the notion of functional similarity in mind. It also eases testing and mocking.

But most importantly, it gives developers the ability to add interfaces post hoc. I can think immediately of several times where we took existing third-party libraries, built interfaces that described their API, and used those interfaces. Later, we could build our own wrappers, adapters, and even replacements using those interfaces... all without ever touching the underlying library.

Conclusion

I don't want to play the part of an apologist for Go. My theory of adopting programming languages borders on the mundane: Choose languages that are useful for rapidly, efficiently, and maintainably building software. Go is one of the most practical programming languages I've ever used. And that's the point I have tried to convey.

Of course it has its weak spots, and of course it is not going to please everyone (especially not purists in either the functional or OOP camps). But the absence of, say, tail recursion or strong inheritance does not make Go a bad language.

Nor do I want to sound like I am suggesting that Go is a solution for all problems. You probably don't want to write GUI applications or embedded systems in Go. You probably shouldn't write an OS kernel in it, either.

But at the end of the day, I find writing (reading, and using) Go code to be enjoyable. And I find that the software I produce in Go is higher quality.