All but the simplest applications borrow code. You could write everything yourself from just core language features but who has time for that? Instead you take on dependencies, pieces of code written by others that usually give us 80% or more of what we need with 20% of the effort. Sometimes these dependencies are made to interact with a specific technology like a database, or perhaps it’s just a library providing some feature that would be onerous to write yourself. The differences are outside the scope of this article. What I would like to concentrate on here is how to use imported code and dependency wrapping in go, while maintaining clean abstractions so that the code base can change over time with your needs.
The Approach
If you need to interact with github for example you have a few choices. You could “shell out” and call the shell client but that’s probably slower than you want, and requires the git client to be installed in the runtime environment. You could use the HTTP API but that will require a lot of boilerplate if you’re calling more than one or two endpoints. The choice for most developers would be to import a library that communicates with github and call it a day. The public library’s quality is likely somewhere between “perfect for my use case” and “works well enough.” It may be well tested, and if not it at least has more users than anything you would write from scratch today. But we still have a problem. While I wouldn’t fault you for betting on git (still) being the version control de facto in ten years, you may switch hosting providers, and most technologies don’t come with the same assurances.
As modern developers, we switch dependencies all the time. The only constant in software is change. Our applications rely on external dependencies like databases, third party APIs, caches, and queues. They serve our needs today but tomorrow we may need a faster option, one that doesn’t cost as much, or a version not tied to a cloud provider. If we want to make these changes without too much pain, or worse rewriting our core business logic, the code that handles a dependency must be isolated. If you are familiar with the hexagonal architecture, often called “ports and adapters,” this pattern may look familiar.
The Queue, as an Example
This feels like a good one because it’s something your application may want to replace in time. Our sample application is a distributed note taking service. For when you need your notes available on eight continents and resilient against global disasters. Our notes application starts with SQS, a queue service provided by AWS, used to notify other services when a note is saved. Error handling removed for brevity.
type Note struct {
ID string
Text string
Created time.Time
}
func sendNote(q, *sqs.SQS, queueURL, n Note) *sqs.SendMessageOutput {
body, _ := json.Marshal(n)
in := sqs.SendMessageInput{
QueueUrl: queueUrl,
MessageBody: aws.String(string(body)),
}
out, _ := q.SendMessage(&in)
return out
}
Introduce a Thin Wrapper
What we want is to get the benefit of someone else’s code without tying ourselves to it. What if we start by providing a thin wrapper around the code we import?
type wrapper struct {
queue *sqs.SQS
queueUrl *string
}
func (w *wrapper) send(msgBody string) string {
in := sqs.SendMessageInput{
QueueUrl: w.queueUrl,
MessageBody: aws.String(msgBody),
}
out, _ := w.queue.SendMessage(&in)
return *out.MessageId
}
type NoteQueue struct {
queue *wrapper
}
func (nq *NoteQueue) Send(n Note) string {
body, _ := json.Marshal(n)
return nq.queue.send(string(body))
}
Now we have two layers here, the `wrapper` type and the `NoteQueue` type, but that isn’t strictly necessary. We could use SQS directly in the `NoteQueue` and still have a clean boundary so long as the SQS details don’t leak into code that uses the `NoteQueue`, though we gain something else in exchange for the bit of extra code. Instead of using the `wrapper` directly we can represent its behavior with an interface.
type NoteQueue struct {
queue interface { // optionally represent wrapper with an interface
send(msgBody string)
}
}