Let me preface this discussion by saying all this has been said before and better. Why am I bothering? Well really this post is about learning things the hard way, something I am definitely an expert at :)
But also, it may give some fresh insight to those starting out and wanting to move beyond simple tutorials.
I recently revisited and overhauled an automation script I use for work. It has been very reliable, at one point it was actually smugly correct about something that I got wrong ha. But at the same time, the code was difficult to modify or even understand! It didn’t really need much modification, more simplification, as the generalised features that I thought I needed originally, I rarely (if ever) used.
The code in question is to keep track of customers and produce PDF invoices. Sadly the repo is not public, as I don’t want to publish customer details. Nothing particularly sensitive, but I’m sure they wouldn’t appreciate me publishing their names and numbers on the internet!
There was another reason, I should confess, it was all an unholy mess under the hood…
Mmm spaghetti code
When I first cooked up this automation script, it was about all I could manage.
I was pleased enough to be able to spit out formatted invoices on demand, with incremented ‘1 of 3’ fields, current dates, and other such conveniences.
I am very much one for learning the hard way. To produce this artefact originally I steamed straight into it, grabbing at anything that worked. I knew it was messy and it certainly didn’t look like the well manicured topiary you can find in some repo’s. The code actually worked well in practice, but had reached the point where I wanted to polish it and add a couple more niceties. A rewrite, famously, is never a good option - plus the ‘business logic’ was already shaped to my needs like a worn old armchair. So the time had come to refactor it all as much as possible.
What I have learnt over time (the hard way)
When I first started out I thought functions were for avoiding repetition - they’re not. Two main purposes:
- functions mean that code only needs to be changed in one place and
- functions track mutations. What is changing where. This is a crucial ‘side-effect’ (not in the comp sci sense) and often beneficial in other ways.
The latter is especially true with a plain value pass/return. Which I now prefer over methods. I would even go as far to say only use methods if you want to be able to compose them, or if you need to implement an interface.
Other ‘discoveries’
- Good names are useful. Following a good style uncovers logic. My favourite advice that I have read on this is from this now slightly outdated repo: variable names should get longer the wider their scope is, and function names should get longer the narrower the scope. Which makes so much sense when you think out it. Of course not everyone agrees - I’m looking at you *ahem* Prometheus.
- Tests are revealing - in the writing alone. It makes you look at your functions from the outside in. Although it has to be said they can be a lot of work and often leave a shimmering doubt in your mind of what they will have missed. They’re also challenging to implement at the edges of your code - input/output - and that is where the real stuff happens. I think I’ve rarely caught a bug in the wild with a test, but that they have nonetheless improved my code and helped me understand it.
Sidebar: So what’s so special about Go?
I feel the elephant in the room here is that one of the authors of Go is Ken Thompson! Ok, fanboi alert… But yeah, B - the precursor to C; erm Unix - the precursor to like everything; and chuck in some UTF-8 without which where would our precious emojis be? 🤪
The language builds on decades of experience, going all the way back to Algol. Which means that there is a lot that Go does that is understated and quiet. If you let it guide you, it helps you write really clean logic. Often simply writing something more cleanly often uncovers issues that were otherwise hidden.
A good approach in general is to apply simple principles and see where it gets you. This is what I call inductive living. Let yourself be guided by paying attention to the simple details. This can feel a bit like you’re not being determined or focused enough, but it’s incredibly effective. Let the Big Picture take care of itself, while you build slowly but surely, brick by brick.
That said, I’m sure Ken would have let everyone run riot with manual memory management if he could have (or perhaps not?) The under-the-hood runtime of Go is probably most useful to enable the snazzy concurrency features. But I’m grateful not to have to remember to malloc and free things. It can also be noted that for this automation script and a lot of Go out there, channels and concurrency were not used once.
Though they have been incredible for Syntə.
Anyway,
So I set about a comprehensive revision of this atrocious code. Incrementally moving forward. Preserving - mostly - the logic. Pulling things apart, shaking down others. But it was a fun, almost relaxing task. I was able to collapse redundant code. The codebase shrank by 25%. All was all laid bare in the process. And in the end I achieved what I set out to - it is now maintainable.
I can now delve in to tweak things, add little features simply, iron out quirks. It’s still not what I’d call polished or rugged, but it’s readable.
Some of the more hairy functions have yet to be cleaned up entirely, and some larger ecumenical decisions are still lurking. But these are now much much more tractable. There is still more learning to be had too. There always is. Which is a good thing.
Go is not only a great language for writing software, but one that allows you to grow.
Syntə itself has followed a similar trajectory, which is visible in the commits.
Aaand relax…
I’m pleased to have reached a place where coding is relaxing.
This doesn’t negate my prior post because I’ve been refactoring. Which is not the same as writing.
If I was starting from scratch I would probably spec it in a very simple pseudo code and then translate into Go. Which has given me an idea… 🤔
The backend shouldn’t lead the frontend
One thing I did get right and I stand by is avoiding a ‘bumpy’ user interface - by which I mean what is natural in code does not necessarily present as natural to a human. I see this all the time in the wild. A UI where you have to go one extra step - a button press, a scroll right, or an unnecessary new page load. I hate this and one of the joys of writing my own software on this admittedly small scale, is that I can eliminate it.
And it’s all about the simple joys of life :)