Last updated 1 year ago by Tit Petricgolang
After writing two books on the subject (API Foundations in Go and 12 Factor Applications with Docker and Go) and years of writing various Go microservices, I do want to put some thoughts down as to what it takes to write great Go code.
But first of all, let me explain this very plainly to all you who are reading this. Writing great code is subjective. You might have completely different ideas as to what is considered great code, and we might only agree on some points. On the other hand, neither of us may be wrong in such a case, we’re just coming from two different perspective viewpoints, and may have chosen to solve our engineering problems in a different way.
This is very important, and you might strongly disagree - if you’re writing Go microservices, you may keep all your code in one package. There are of course very strong and valid viewpoints to the opposite, some of which are:
If you’re counting, the minimum package count for a microservice is 1. If you have a large microservice, with websocket and HTTP gateways, you may end up with a minimum of 5 packages (types, repository, service, websocket and http packages).
The simple microservice doesn’t really care about abstracting business logic away from the data storage layer (repository), or even from the transport layer (websocket, http). You write your code, it digests data and spits out your responses. However, adding more packages in the mix, solves a few issues. For example, if you’re familiar with SOLID principles, the ’S’ stands for “single responsibility”. If we break everything down into packages, these become their responsibilities:
types- declares structs and possibly some mutators of these structs,
repository- it’s a data storage layer that deals with storing and reading structs,
service- would be the implementation of business logic that wraps repositories,
websocket, … - the transport layers, which all invoke the service layer
Of course, depending on your use case, it may make sense to break these down even further, for example you could have
would separate some structs better. This way you can have
response.Message instead of
MessageResponse. It may
make more sense if those have been separated from the start.
But, to push home the original point - don’t feel bad if you’re using only some of these package declarations. Big software like Docker uses only a types package under it’s server package, and that’s all it really needs. The other packages it uses (like a global errors package), may just as well be a third party package.
It’s also worth noting that it’s much easier to share the structures and functions you’re working on, if you’re living in the same package. If you had structs that depend on each-other, spliting them up into two or more different packages might lead you to encounter the diamond dependency problem. The solution for that one is relatively obvious - rewrite your code to be stand-alone, or make all your code live in the same package.
So which? Both ways work for me. If I’m being fully pedantic about it, splitting it up into more packages makes it cumbersome to add new code, as you’ll likely have to modify all of them to add a single API call. Jumping between packages may be a bit of a cognitive overhead, if it’s not very clear how you’ve laid them out. Intuition can only take you so far, so in many cases you’ll easier navigate the project, if it only has one or two packages.
You definitely don’t want to go with many small packages either (aka “Tiny Package Syndrome”).