Write DRY Go code with Generics
by Hagai Dayan
Golang version 1.18 brought a shiny new feature — Generics.
Generics is a programming style that is known and common in other high-level languages, including Python, Java, C#, and many more.
In short, the approach is “write code that operates on types that will be determined later.”
The word “later” here means during run time rather than compile time.
In Mitiga.io our core backend technologies are Golang and Typescript.
Both technologies support generics, and both are typed.
We use the type language feature to create API “contracts“ between the microservices in the system.
Let’s see what it looks like in Golang
As you can see, the function genericPrint
operates on every type it gets using the arbitrary type any
as a receiver.
The fact that this function works as intended is based on the assumption that fmt.Printf()
receives interface{}
type as an argument.
Therefore it has the ability to function properly for every type given as an argument.
cool! can we do some more useful stuff with generics?
check out the following function -
What motivated the software engineer who wrote this code is the ability to support all sorts of types with operation ‘+’ and make their code less repetitive.
This principle is called DRY (Don’t Repeat Yourself)
DRY is an important software engineering principle that points out the downsides of code duplication.
When possible, every piece of code should have a single, unambiguous, authoritative representation within the system.
If it doesn’t apply, every time you need to make a change in the behavior of the system it requires multiple changes in the code.
Since most of the time DRY is a great rule to follow, in theory the function above is better than the following two functions:
HOWEVER, genericAdd
will not work due to a compilation error.
Languages that aren't typed (such as Python) do allow this sort of function.
And what happens if the program does not find implementation for operator ‘+' of type T at runtime?
The program crashes - which is probably not the software engineer’s intention.
Golang is a typed language and its compiler allows these sorts of functions only as long as the receiving type supports the ‘+’ operation.
So what is Generics in Golang good for?
Let’s check out the following program.
The program reads a file that is consisted of a list of Chrome explorer User-Agent objects represented in a JSON format.
The program analyzes the User-Agent fields and writes them in a database.
Each Chrome User Agent object consists of the fields apple-web-kit
and event-time
.
Pretty straightforward, huh?
Hold tight -
Now the program is required to support another use case — FireFox User-Agent objects.
FireFox User-Agents objects support a different field — gecko-version
.
Let’s suggest a simple solution. We can add the following code:
This addition to our code works just fine, however, it does not comply with the DRY rule.
Can you spot the code duplication?
For starters, see how struct field EventTime
repeats itself in both structs ChromeUaFields
and FirefoxUaFields
.
Let’s make the code DRYer by using Golang’s feature Struct Embedding:
Now the structs’ scheme looks as DRY as it gets.
But how can we make the parts of ParseFireFoxExplorerUserAgent()
ParseChromeExplorerUserAgent()
DRY?
Can we perhaps unify them to only one function?
We can! Let’s use Generics!
See how we not only created one function to support both use cases, we also added the type webExplorerUaArgs
. That type acts as a superset of concrete web explorer User Agents, which is actually quite handy in case we need to address all of the concrete types together in the future, in other generic use cases perhaps.
Finally, let’s show how we can use the design patterns we implemented to easily support another web explorer — Edge:
Win-win situation in Go Generics
In most languages, Generics should be used wisely and carefully. In Golang, we have a strong compiler that prevents us from misusing it in many cases.
As a result, we get a much less error-prone program.