Writing Better Go: Code or Data?

A Journey...

I've been on a bit of an odyssey trying out various languages in case there is something out there that might suit me better than Go.  After considering many others - and actually trying out C#, F#, and Kotlin - my feeling is that I will not be abandoning Go in the short-to-medium term (although I may well delve deeper into Kotlin).

However, as with many journeys, much of the benefit comes from the travelling itself rather than achieving a destination.  For the purposes of this exercise I ported a little utility of mine which unpacks a legacy file storage format to each of the aforementioned languages and tried hard to write idiomatically in each one.  Doing this inevitably leads one to question the structure of the original code and also any ingrained habits that might have passed their use-by date.

...and An Outcome

If I am sticking with Go, I really ought to think about trying to write 'better' Go.

A Case in Point

One of my pet peeves about Go is its lack of enumerated types.  As I was researching idiomatic enum-like patterns in the three candidate languages I came across several opinions that over-use of enums was a 'bad smell' in code.  Some of the people who hold this opinion this seem to me to be worth listening to so, even though I could find very little by way of justification for this assertion, I thought maybe it was time to reconsider some old habits.

Old Dogs

[Cue the violins, you can skip this bit]

At school we accessed the county mainframe via a teletype, in my first year of Comp.Sci. at university we used punched cards, only in the second year did we get access to VDUs and personal computers.  We thought about memory in terms of kilobytes (or even kilowords) and processing speed in terms of Dhrystones, Whetstones and Transactions Per Second.  Whilst we were aware of the dire consequences of premature optimisation, we regarded them as primarily a warning against excessively 'tricksy' code that would be hard to read and maintain.  We naturally wrote code that was conservative in its use of run-time resources (but not at the expense of readability).  I still do.

Old Tricks

For years I have used constants and enums wherever practical to try to make my code safer and to help compilers produce efficient code.  By and large this has worked well for me.

Here is a some of the code I had before starting this project:
It's a very familiar pattern; basically, a list of consts followed by a big switch statement to handle each case as defined by the consts.  This pattern is highly prevalent in C systems programming code and APIs - a lot of my Go coding involves using or porting such software.

(Not so) New Tricks

Here is a new version of the code:
I would contend that this is both shorter and better code.

What Have I done?

Nothing clever, it's not rocket science.  In a nutshell I have replaced an abstract list of consts with a meaningful map describing each entry type and its attributes, then used that map to conflate the switch statement to a simple if... else... dealing with known and unknown cases explicitly.

A lot of the case-handling has moved from procedural code into the data itself.

Why Is It Better?

Do you need to ask?  The logic is so much clearer and adding new types (which will happen) now means adding a single line of code/data for each type, rather than at least three for the old approach.

Also, there is a kind of 'completeness' about the handling now that Go cannot provide with switches and constants.  (Some other languages can provide this kind of assurance with rich enumerated types and switches.)

Any Downsides?

The main question in my mind was performance (see above); I remember when associative arrays (i.e. maps) came with quite a performance penalty.  Fortunately, others have already investigated switch vs. map behaviour in Go, and it turns out that there is a small penalty in trivial cases, but the map approach actually wins (and is almost constant) for randomly accessed maps over a certain size.  See Jack Christensen's write-up for more details, but note that the Go compiler has improved since then and the break-even point has improved somewhat.

Moving Forward

I need to guard against blindly reusing old patterns when there are clearly better alternatives.  Time to review a lot of my code...




Comments

Popular posts from this blog

Terminal Emulation - Fun with a Go GUI

Lessons Learnt: Porting an application from Go to Ada