software engineer at auth0, writing code for humans, with humans.

When I came from the very dynamic world of ruby, my first approach to designing HTTP clients in go was to think about mocking similar to what I was used to in Ruby. I quickly learned that it’s not the ideal approach in Go, and over time have learned a few tips and tricks for dealing with HTTP clients.

Hypothetical Client: Runtime

Let’s imagine we have a hypothetical Runtime client with just one public method:

If the implementation is mostly executing an HTTP request against a URL, one can imagine the following simple implementation:

This all makes sense so far, and is straightforward. …

When I started picking up the go language years ago, things that stood out to me immediately were the io and net/http packages. Both of those packages were extremely well done and minimal in the amount of interfaces they exposed to the user.

What I love about net/http

The two things which stand out for me are:

  1. Interface Design.
  2. Composability

Interface Design

The primary constructs you’ll interact with are pretty limited:

To build your service:


To handle requests:


To build a client:


In total, we’re looking at 7 core primitives to build a client and server — which isn’t a lot! …

When I mention dependency injection to folks who have Java or .NET backgrounds it usually invokes a few sets of questions — to which my usual response is:

It’s likely not as sophisticated as what you’re thinking — and it doesn’t have to be!

While there are methods to use more sophisticated approaches in Go, starting off projects the simplest way possible is still the preferred approach.


Dependency injection represents the D in SOLID principles. Quoting from wikipedia:

In object-oriented computer programming, SOLID is a mnemonic acronym for five design principles intended to make software designs more understandable, flexible, and maintainable. …

One of the most prevalent patterns when managing the lifecycle of goroutines in Go is to have a done channel. Here’s a quick example showing this in action:

done := make(chan struct{})for {
select {
case <-done:
case job := <-jobs:
// execute job

In fact, this is pretty much how the Done channel in context is done under the hood.

Approaches to closing this done channel

Option 1: using a mutex

If we take a look at how it’s done in the context package, it simply uses a mutex to ensure that the action of closing the channel is only done once.

Here’s the relevant snippet showing…

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store