patterns in go
In go there are some patterns that make code idomatic, these are some of my favourite…
strategy pattern
“Enables an algorithm’s behavior to be selected at runtime.” Wikipedia
This pattern allows you to pass structs, or objects around that have slightly different runtime behavior without having to worry about the difference. There’s a great talk by Dave Cheney about functions as first class citizens. This talk does a great job of explaning why and how to use functions in some places instead of interfaces, and this is closely linked to the strategy pattern.
Functions work really well with this pattern and allow different implementations to be passed in at runtime.
In the following package safe
, the struct Str
holds a string, and implements the Stringer
interface:
safe.go
|
|
Str
also holds a function to define how to mutate the value when String()
is called.
Both safe.Lower
and safe.Obfuscate
satisfy the mutator function type definition, so can be used as mutators for safe.Str
. safe.NewStr(...)
returns a new safe.Str
with a value and a mutator for printing it out.
One of these implementations or ‘strategies’ could be chosen at runtime to change the behavior of an app.
main.go
|
|
Here we can see that the user
and pass
can be passed around without having to worry about the values that they hold.
They can be printed and logged with out worry as the String()
method will call Str.mutate()
and print out the value in a safe way. In this case; not revealing the password.
|
|
Try it out in the go playground
middleware
There are two obvious uses of middleware in go webapps. The first is incoming, wrapping incoming requests with instrumentation or context or logging etc. There are many packages that do this, e.g. the new relic agent.
One way to achieve this is chaining the http.Handler
interface. This interface has a single method ServeHTTP
.
Your example logging middleware could look something like:
|
|
This returns a new http.Handler
that executes custom logic before calling next.
The second is outgoing, creating an http.Transport
that implements the RoundTripper
interface, here you can add your middleware logic.
|
|
Then to use it, create an http.Client
that uses the new customTransport
…
|
|
struct vs. data structure
There is a big debate around whether a class or struct is a data structure, but let’s not get into that here. In go there are a number of usecases where you could use a standard library data structure but a struct is often better. Given the following JSON (which describes a meal):
|
|
This could be marshalled into map[string]interface{}
but that’s rubbish! Instead let’s use structs and named types:
|
|
We have given our struct some tags to describe the json field name.
Here we create a named type Ingredients
which has an underlying type of []string
, and use composition to put that inside a Lunch
struct.
Then we can take our json (with maybe came from a POST request) and unmarshal it.
|
|
Sure, we could have used that map[string]interface{}
from before, but you wouldn’t have the ability and knowledge of what is stored in the interfaces, and nothing to enforce that the incoming json is of a valid form.
We now have the power to iterate Ingredients
or access lunch.type
etc.. and we know what fields could be there.
|
|
output:
|
|
Example in the go playground
memoisation
“Memoisation is an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again” - Wikipedia
Go is really good at networking, and in distributed architectures it’s easy to end up in a situation where you need to cache the values of expensive external calls. There are obvious many libraries and existing projects taht can help to do this, but it’s always useful to understand what’s going on.
This is a very simple, rudimentary example, we have a datastore to store our cached values, and a function that knows how to execute the expensive call. (because maps are not threadsafe in go, we have a mutex)
|
|
Here we can see that if the expensive result is not in the cache then calculate it and add it to the cache. This is a contrived example but it shows how the process could work.