More Golang adventures
I recently wrote a little application called ConfigServer (although the project will probably be renamed). It is intended to be a tool to assist release managers and software deployments by providing a central server to hold configurations, but also allow configurations to be version controlled. ConfigServer provides some inheritance capability, the idea being that you can have a Cluster configuration, a Server configuration that inherits properties from the Cluster, and an Application configuration that inherits from the Server – ConfigServer doesn’t care about the schema, actually, but that’s an implementation detail.
The original version of this application was written in Ruby, a language that I was once extremely well-versed in, but which I haven’t used for anything but one-off command-line scripts in a few years. It took me about 6 hours to write the thing.
Out of curiosity, and since the project was so small, I decided to re-implement it in Go. When I started the rewrite, it was the second time I’d looked at Go syntax; the first was to fix a small benchmark program that was comparing Go to Vala (commentary on that is here), so it really was the first time I’d written any Go; I was utterly unfamiliar with the packages. The Go version took me about 20 hours. That time includes unit tests and a couple of refactorings to improve segregation of responsibility; I guesstimate that if I performed the same refactoring and writing unit tests for the Ruby code, I could probably spend another 6-10 hours; on the one hand, I know the shape of the modules/packages, but on the other hand, writing unit tests always takes a lot of time.
cloc tells us this:
Language | files | blank | comment | code |
---|---|---|---|---|
Go | 7 | 104 | 43 | 482 |
Ruby | 1 | 59 | 55 | 326 |
This excludes unit tests. So here are my observations:
- I’m frankly amazed that the Go LOC is so close to the Ruby LOC (33% larger). If I refactored the Ruby code to be more well-structured and encapsulated (and unit-testable), that LOC delta would shrink even more.
- The Ruby program is a single, executable-and-library file. This was intentional. I was able to break up the Go code into more files because the end artifact would also be a single executable.
- The Go compiler is wicked, stupid fast. [gomake]{.Apple-style-span style=“font-family: ‘Courier New’, Courier, monospace;”} is nice, in that it uses standard Makefiles but provides includes to greatly simplify the Makefiles.
- The Go unit test package is anemic, providing only the bare minimum of functionality.
- Go has such potential for mixing in, but the language spec disallows it (you can only define functions on structures that are within your own package).
- Even if I were as knowledgeable about Go as I am about Ruby, I’d still have been able to whip the Ruby version out faster. On the other hand, the Go version is much safer, and I trust that program a lot more – it is, after all, type-checked. I guarantee that no matter how much I look at the Ruby code, if it runs for any length of time I’ll eventually encounter a typing error because of a bug in the code.
- Go has an amazingly useful set of built-in libraries, without being package and class bloated (like, say, Java). Although, the crypto package is curiously huge.
- The Go package documentation, FTW!
- Doing system exec stuff is easier in Ruby, but not by a lot.
All in all, Go surprises me; it’s concise, easy, encourages good
separation of concerns, is easy to read, and much more functional in
feel than you’d expect from looking at the tutorial. At the same time,
it has some unfortunate quirks: it’s (currently) relatively slow for a
compiled language, and builds large executables; some language
constructs are just baffling in their constraints, such as
range
only being applicable to a small set of built-ins, rather than working on some
well-defined interface; tail-call optimization is extremely limited,
which is such a shame for a language as close to being a functional
language as Go is; and the lack of ability to mix in to extra-package
types is limiting.
To the first point in my list of Go quirks, I have no doubt that this will change rapidly, and Go will catch up; it’s a very young language, after all. As to the last point, the explanation put forth by the Go developers is, frankly, weak. It’s the same argument Gosling made about not supporting multiple inheritance in Java, which basically boils down to “programmers are stupid, and this feature will confuse them.” I don’t buy it; I’d rather have extra-package mix-ins with limitations based on technical merit than none at all.
I’ve learned that I vastly prefer the cost of slower development time to gain reliability and safety, as long as the cost isn’t too high. Haskell is even safer, but it pushes the boundaries of how much pain I’m willing to endure trying to get code to pass through the type checker.
Right now, I’m thinking Go’s a keeper. It is more productive than C, more reliable than Ruby, easier than Haskell, is superior to Erlang in a whole host of ways I won’t enumerate here (although, I do like the features that the OTP brings to the table, many of which are “killer” features unavailable in any other language, which keeps Erlang highly relevant), and is far better than Java in terms of simplicity, syntax, typing, threading, native compilation, and memory use. Each of these languages surpasses Go in their own ways, but I believe that Google has successfully found that sweet spot in a practical language that minimizes the annoying attributes and boiler-plate, and provides strong typing, resulting in a highly “writeable” language that compiles to native executables. We’ll see how I feel with a few gross more hours under my belt.