« Back to home

Go: Some lessons learned

Posted on

Based on my experience building, deploying and maintaining a small web application for work purposes, I’ve assembled some thoughts on Go.

Design patterns

I started out with little code organization in terms of files and directories. I eventually migrated to model, view and controller subdirectories, plus an internal/test directory for test utilities.

I’m not a big believer in ORMs. I’ve tried them in the Java world, and found that the benefits aren’t enough to make up for the downsides, at least not given my reasonable SQL-query-writing abilities. I eventually settled on a DAO model, with lightweight core object classes and FooMapper DAOs.

Verb choice

I think DAO verbs should match REST verbs. It’s tempting to implement UPSERT and use it for both PUT and POST, but then your REST API ends up overly forgiving.

The downside of using REST verbs is that it pushes some of the problem onto your service consumers – they need to know whether they’re writing an object which already exists, or a new one. On the plus side, it makes the DAO code pretty trivial.

Method naming

I’ve found that a good naming pattern is to include the class name in the DAO method name if the method takes an ID as argument, but to exclude it as redundant if the method takes an object argument. This also means you can have both Delete(Foo) and DeleteFoo(id).

So I ended up with

  • NewFoo (or just Foo{})
  • GetFoo
  • DeleteFoo
  • Put(Foo)
  • Post(Foo)

Go vs Java

Avoiding cyclomatic complexity seems to be much easier in Go than in Java. In Java, I regularly get PMD complaining about method complexity > 25; in Go, my highest (excluding test code) was 9.

I spent more time worrying about how to structure my code, and less time actually coding or working out how to do things. Eventually I’ll learn the Go patterns, but for now it’s all different. So, don’t expect to use the same patterns you would in Java or Ruby. For example, in Java it’s considered a mortal sin to expose fields directly. In Go, it’s standard practice, if you look at the standard library.

If something ends up being complicated, you’re probably doing it wrong. This is a very generally applicable rule, but particularly relevant when moving between programming languages with very different paradigms. For instance, I kept forgetting that in Go, you don’t declare that you’re going to implement an interface – you just do it.

Similarly, when I started writing my web service handlers, I found myself frustrated by the lack of generics. I soon had a ton of code working around the lack of generics using a shared interface, factories to instantiate objects, and long switch statements. After that, I used currying to build a function which would build a function to perform operations on an object of a given type, using reflection where necessary. After that, I ripped out all the clever code and just wrote plain old classes which implement the same interface. It would be nice if Go had generics to enforce that the classes have the same interface, but it’s not a huge deal.

How to learn Go

Follow a learner’s path:

  1. Implement minimal functionality.
  2. Implement unit tests for 1.
  3. Lint 1 & 2 until you’ve resolved all issues.
  4. Iterate.

Think interfaces first, and make your interfaces small.

And of course, every time you find a bug, try to add a unit test that would have detected it.

Antipatterns

If you return x, error, error should always be non-nil if x is invalid. Otherwise, you’re just asking for nil pointer runtime crashes.

For a field to be serialized as JSON or XML, it needs to be exported – but that means it has to be visible to users. If you have a Java background it’s tempting to have a hidden field and getter/setter pair, but then you need to write your own custom serializer. The Go way is to simply make the field visible and assume users will behave responsibly.

If you are building a web service, it might be tempting to try to mock the database and mock up HTTP requests for your unit tests. Then you realize you need to bypass the router, and pretty soon your test code is longer than the handler method you’re actually testing. Better to do a full functional unit test including router and database components. After all, a bad route change can kill your application as dead as a handler error.

Don’t try to build class relationships using struct embedding. Trust me, that way lies madness.

Don’t try to work around the lack of generics. If you can’t come up with a single interface, just go ahead and build independent classes.

Annoyances

There’s no Date type, so if you want one for your database records you’ll have to build it yourself.

End thoughts

After a long time in the Ruby/Rails and Java/JSF world, Go feels extremely “bare metal”. No login system provided, no flash messages, no sessions even! It can certainly trigger some impostor syndrome feelings.

In the end, though, there’s the benefit that there’s no “magic” being applied; I’ve found that magic do-it-for-you code often misbehaves, and when it does you can be powerless to fix it.

Ultimately, I still like Go, and want to carry on working with it. The radical simplicity sometimes makes for some annoyances, but it has upsides.