mail

Dependency injection is the ability to change types and methods at runtime. It can be used to provide a logger or a connection pool to a library or a module. It can be used to manage the configuration of a service and choose between different implementations. It is also used to mock a service and make unit test easy and decoupled. This article digs into dependency injections with Go.

TL;DR With Go, dependency injection should be visible in the code. As a result, the code should be written to enable it. There are several ways to dependency injection: this article provides some examples that rely on function parameters, containers, closures... It also present a few alternatives you might want to avoid.

Go is not magic

Let's start with some magic code that involves dependency injection and Node. Let say you have a simple library that provides a function that returns hello like below:

// hello.js file
const hello = () => {
	return "hello"
}

module.exports = hello

Then you have an index.js script that get the function and use it:

// index.js file
const hello = require("./hello")

console.log(hello())

As you would guess, running the index.js script would probably return "hello". Try node index.js! Then comes the magic, if you run some secret command before actually running the code, you can change the behavior of your call. Below is an example:

// index.js file
require("./inject")
const hello = require("./hello")

console.log(hello())

It could very well be that the node index.js return goodbye when, for instance you have inject.js that contains the code below:

// inject.js
var mock = require("mock-require");

mock("./hello", () => {
  return "goodbye";
});

You may think, this is very clever! Actually Go does not allow this kind of construct and, when it does, like with global variables and init() functions, magic is discouraged. So you may think, it would be helpful to write code. Don't, and think how useful it is to not have those constructs when you read code. And it does not matter if that for reading the code from someone or yours, in 3 weeks from now.

Functions as parameters

The simplest way to perform dependency injection is to pass your dependencies as a parameters. Let's say, for instance that you have a function that prepares a message and sends it. You would like the function to be able to send the message via an HTTP POST or via an email. You also want to be able to test your function and, for that purpose to mock the sending.

So the way you can do that is by using a parameter to get the send function from outside. The code below shows how you can create such a function and test it with a mockSend() function :

package main

import (
	"fmt"
)

func mockSend(msg string) {
	fmt.Println(msg)
}

func prepareAndSend(f func(string)) {
	msg := fmt.Sprintf("message")
	f(msg)
}

func main() {
	prepareAndSend(mockSend)
}

As you can see above, thank to first class function in Go, you can inject one function as a parameter of another one. As a result you can write a pass the dependencies directly as parameters.

Interface as a parameter

It might be that instead of having one function as a dependency, you want t several. For instance, you could have a function that relies on a set of 4 functions named Create, Read, Update and Delete. If that is the case, obviously you could rely on 4 parameters to inject your dependencies...

A more common pattern is to rely on an interface, instead of a function. Below is a simple struct that implement an interface and that is used in another function:

package main

import (
	"fmt"
)

type gopher interface {
	string() string
	sendMSG(string)
}

type mockGopher struct{}

func (a *mockGopher) string() string {
	return "Rob Pike"
}

func (a *mockGopher) sendMSG(msg string) {
	fmt.Println(msg)
}

func prepareAndSend(g gopher) {
	msg := g.string()
	g.sendMSG(msg)
}

func main() {
	prepareAndSend(&mockGopher{})
}

Receivers are parameters

Before we go on, let's outline that a receiver is actually the same as a parameter. As a matter of fact, there is a syntax that pass the receiver as the first parameter of a method:

package main

import ( "fmt" )

type team struct {
	gophers []string
}

func (t *team) String() {
	for _, v := range t.gophers {
		fmt.Println(v)
	}
}

func main() {
	t := team{"Rob Pike", "Ken Thompson", "Robert Griesemer"}
	(*team).String(&t)
}

Struct as a receiver

Now imagine your function does not only rely on an few methods from the same package but instead ondependencies from several packages. You would find that the frequent way to manage dependency injection is actually by using a struct type as a parameter. Each field of the struct could itself be an interface. The parameter is known as the dependency container and the most common pattern is that the container is actually implemented as a receiver. Below is an example where:

  • container is the struct used to hold the dependencies. The actual code is a method named prepareAndSend
  • the dependancies are actually stored in the container. In that case, they are implementing the gopher interface and, in the case of the example, that is the mockGopher struct that is being used:
package main

import (
	"fmt"
)

type gopher interface {
	string() string
	sendMSG(string)
}

type container struct{
	gopher gopher
}

func (c *container) prepareAndSend() {
	msg := c.gopher.string()
	c.gopher.sendMSG(msg)
}

type mockGopher struct{}

func (a *mockGopher) string() string {
	return "Rob Pike"
}

func (a *mockGopher) sendMSG(msg string) {
	fmt.Println(msg)
}

func main() {
	c := &container{&mockGopher{}}
	c.prepareAndSend()
}

In the example above what you see is that you can initialize the container with the dependencies of your needs so that prepareAndSend behave as expected.

Closures

Another common way to inject dependencies is to create a closure instead of a struct and a method. Closures are anonymous functions that embed variables defined out of their scope. The benefit is that they are usually shorter to define and easier to test. They remove the need for a container struct.

Below is the same example as the previous one that relies on a closure that is generated by the generatePrepareAndSend function. The closure is equivalent to the prepareAndSend function from the previous example:

package main

import (
	"fmt"
)

type gopher interface {
	string() string
	sendMSG(string)
}

func generatePrepareAndSend(gopher gopher) func() {
	return func() {
		msg := gopher.string()
		gopher.sendMSG(msg)
	}
}

type mockGopher struct{}

func (a *mockGopher) string() string {
	return "Rob Pike"
}

func (a *mockGopher) sendMSG(msg string) {
	fmt.Println(msg)
}

func main() {
	generatePrepareAndSend(&mockGopher{})()
}

Closures are often used as a singleton function like in the case of an HTTP middleware but they do not have to.

Other approaches

As a matter of fact, you will find other ways to manage dependency injection. They are usually linked with some requirements for a large number of dependencies like google/wire, uber-go/dig and a number of libraries named dingo like elliotchance/dingo. They could be nice solutions but, unless you also have some specific requirements, you probably do not need them.

Last, you might and please don't, consider another pattern where instead of injecting the dependencies, you lookup for them, for instance from a global registry. ServiceLocator is a complex to read and complex to test pattern, so unless you have a very good requirement for it, you probably do not need it.

Enjoy clear, non-magical, code!