Dependency Injection in Go (Golang)

Are you trying to figure out how to do dependency injection in Go (Golang) to make testing easier and allow you to switch providers? Maybe you want to create an app that you can run on Google’s AppEngine to take advantage of their PaaS datastore and other features but you want to have the ability to swap out the storage and run it on AWS against DynamoDB?

You just know you need a Dependency Injection library don’t you? Well, I’m going to try and show you that you probably don’t.

An easy mistake to fall into when you switch to a new programming language is attempting to map all your previous approaches to it. You have a bag of tricks and know-how that you have built up over many years and they have served you well so they should all still apply … right?

Well, some do and some don’t. The principles behind the practices usually carry over but their implementation is usually driven by the constraints of the system you are working on - both the features and the limitations.

A couple of years ago I switched a lot of my development from .NET to Python and I learnt a lot of new things and new approaches which I think made me a better developer. It’s not that one was better or worse than the other - they are simply different and having more experience of more environments and approaches is always better. Trying to write Python code with a C# mindset or vice-versa doesn’t produce the best results though.

Recently, I’ve been doing more work with Go (Golang) and have been blown away by the performance it gives you as well as the clean simplicity of both the language and the ecosystem - lots of good decisions being made to keep things on the straight and narrow and avoid teams wasting time discussing things like code formatting etc…

It has highlighted again though the need to take a step back, remember the basic first principles and not try to find the equivalents of libraries and packages that you remember from other platforms.

Go seems to favour simplicity and clarity. When I look at .NET or Java systems they almost seem laughable at how much infrastructure code is needed to do sometimes basic things. I think a lot of this has developed over time as lots of good tools have been developed for very good reasons but at some point programming in the language becomes programming in all those other libraries as well - they take over and you have to become an Autofac ninja or Spring guru to try and understand what the app does.

Let’s forget about all the lifecycle nonsense for a moment and go back to the basics. The important thing is Inversion of Control, not Dependency Injection which is just a method to achieve it. The important thing is to reverse the direction of control flow by providing dependencies instead of code creating them itself and being tightly coupled. Interfaces enable us to do this so that we can change the implementations that we pass in. They also factor into other SOLID principles as well.

Go has first class support for interfaces that seem to site really nicely between Pythons “an interface - what’s that?” and C# / Java’s “thou shalt inherit from me if you need to look like me” approach. They are implicit which means that anything that satisfies the method signature can be used via the interface. It also has first class functions which can be passed around and set as variables. Together these can be used to make things nicely pluggable.

Anyway, enough talk - let’s code. We’ll start with something basic - logging. If you write code for app engine then you’ll know that it comes with some logging services which you can access via the web console. This is shown in the terminal when your app runs on a development server.

If you are running your app outside of app engine you may want to write straight to the console or use a completely different logging service instead and you probably don’t even want to pull in the app engine packages.

If you are being very thorough you may want to test all your functions behaviour including the log messages it creates so need an in-memory capture or, when testing other functionality, you just want to pass in a do-nothing stub instead.

Let’s start by creating an interface and a factory method to create an instance of a logger (/logging/api.go):

package logging

import (
	"golang.org/x/net/context"
)

type (
	Logger interface {
		Debug(c context.Context, format string, args ...interface{})
		Info(c context.Context, format string, args ...interface{})
		Warning(c context.Context, format string, args ...interface{})
		Error(c context.Context, format string, args ...interface{})
		Critical(c context.Context, format string, args ...interface{})
	}

	loggerFactory func() Logger
)

var (
	New loggerFactory
)

The Logger interface is fairly straightforward and defines some method signatures for different logging outputs. The loggerFactory is a method type that creates an instance of something that implements Logger. When we import our logging package we’ll be able to call: logging.New() to get a new logging instance.

Let’s start with the app engine implementation as it’s quite simple. It’s really just an adaptor class to map calls from out own Logger interface to the very similar app engine service available through the Context (/logging/appengine.go):

// +build appengine

package logging

import (
    "golang.org/x/net/context"
    "google.golang.org/appengine/log"
)

type (
    appengineLogger struct{}
)

func NewAppengineLogger() Logger {
    return &appengineLogger{}
}

func (logger *appengineLogger) Debug(c context.Context, format string, args ...interface{}) {
    log.Debugf(c, format, args...)
}

func (logger *appengineLogger) Info(c context.Context, format string, args ...interface{}) {
    log.Infof(c, format, args...)
}

func (logger *appengineLogger) Warning(c context.Context, format string, args ...interface{}) {
    log.Warningf(c, format, args...)
}

func (logger *appengineLogger) Error(c context.Context, format string, args ...interface{}) {
    log.Errorf(c, format, args...)
}

func (logger *appengineLogger) Critical(c context.Context, format string, args ...interface{}) {
    log.Criticalf(c, format, args...)
}

func init() {
    New = NewAppengineLogger
}

The init() function runs when the package is first imported and sets the New factory method to the method that creates an app engine specific instance.

Note the ‘build’ tag at the top - this file will only be built as part of the package when running under app engine. We can use a build tag that will include a different file when we are not running under app engine.

// +build !appengine

package logging

import (
	"golang.org/x/net/context"
	"log"
	"os"
)

type (
	consoleLogger struct {
		debug    *log.Logger
		info     *log.Logger
		warning  *log.Logger
		error    *log.Logger
		critical *log.Logger
	}
)

const (
	CLR_0 = "\x1b[30;1m"
	CLR_R = "\x1b[31;1m"
	CLR_G = "\x1b[32;1m"
	CLR_Y = "\x1b[33;1m"
	CLR_B = "\x1b[34;1m"
	CLR_M = "\x1b[35;1m"
	CLR_C = "\x1b[36;1m"
	CLR_W = "\x1b[37;1m"
	CLR_N = "\x1b[0m"
)

func NewConsoleLogger() Logger {
	return &consoleLogger{
		log.New(os.Stdout, CLR_0, log.Ldate|log.Ltime|log.Lmicroseconds|log.Lshortfile),
		log.New(os.Stdout, CLR_G, log.Ldate|log.Ltime|log.Lmicroseconds|log.Lshortfile),
		log.New(os.Stdout, CLR_Y, log.Ldate|log.Ltime|log.Lmicroseconds|log.Lshortfile),
		log.New(os.Stdout, CLR_R, log.Ldate|log.Ltime|log.Lmicroseconds|log.Lshortfile),
		log.New(os.Stdout, CLR_C, log.Ldate|log.Ltime|log.Lmicroseconds|log.Lshortfile),
	}
}

func (logger *consoleLogger) Debug(c context.Context, format string, args ...interface{}) {
	logger.debug.Printf("DEBUG: "+format, args...)
}

func (logger *consoleLogger) Info(c context.Context, format string, args ...interface{}) {
	logger.info.Printf("INFO:  "+format, args...)
}

func (logger *consoleLogger) Warning(c context.Context, format string, args ...interface{}) {
	logger.warning.Printf("WARN:  "+format, args...)
}

func (logger *consoleLogger) Error(c context.Context, format string, args ...interface{}) {
	logger.error.Printf("ERROR: "+format, args...)
}

func (logger *consoleLogger) Critical(c context.Context, format string, args ...interface{}) {
	logger.critical.Printf("FATAL: "+format, args...)
}

func init() {
	New = NewConsoleLogger
}

Again, this is an adaptor class that this time maps calls through the interface to the standard console output with some nice colors thrown in for good measure. As before, we use the init() function to wire up the factory method to use our implementation. This could just as easily be turned into a client for a 3rd party logging service as required.

In our calling code we can now simply import our “app/logging” package and create and use logging instances as needed (the c context.Context is created elsewhere in the app) with the correct implementation being used, e.g.

log := logging.New()
log.Debug(c, "requested %s", name)

If we don’t have a clear 1:1 match between implementations and platform then we can’t rely on the single init() function to set the correct factory and instead would need to set the logging factory method to use as part of the app initialization, most likely based on some flag or environment as per 12-factor app practice. We would use the same thing in unit tests to set it to use whatever instance we wanted.

type (
    nullLogger struct { }
)

func NewNullLogger() logging.Logger {
    return &nullLogger{}
}

func (logger *nullLogger) Debug(c context.Context, format string, args ...interface{}) { }

func (logger *nullLogger) Info(c context.Context, format string, args ...interface{}) { }

func (logger *nullLogger) Warning(c context.Context, format string, args ...interface{}) { }

func (logger *nullLogger) Error(c context.Context, format string, args ...interface{}) { }

func (logger *nullLogger) Critical(c context.Context, format string, args ...interface{}) { }

logging.New = NewNullLogger

I hope that is clear enough and of some value. I’m using the same technique to enable repositories based on the app engine datastore to be swapped out with in-memory versions when working on unit tests (integration tests can still run against the development datastore) and it seems to be working well. I could easily envisage having a config switch to set the implementation to use at app-startup and be able to create SQL or DynamoDB implementations if I wanted to run my app on a different platform instead.

The key thing is to use interfaces and factory methods so none of your code is ever directly coupled to any specific platform implementation.

comments powered by Disqus