【Golang】Dependency Injection in Golang

Posted by 西维蜀黍 on 2021-08-18, Last Modified on 2021-10-17

Without frameworks

Dependency injection is passing a dependency to another object or structure. We do this as it allows the creation of dependencies outside the dependant object. This is useful as we can decouple dependency creation from the object being created. Lets look at an example of dependency injection in Go:

type DataStore interface {}

type PersonResource struct {
	Store DataStore
}

func New(store DataStore) *PersonResource {
	return &PersonResource{Store: store}
}

In this example we have a structure called “PersonResource”, this depends on a “DataStore” type. We are passing in the DataStore upon creation of our “PersonResource”. This allows any datastore we want to be used inside our PersonResource as long as it implements DataStore. An example use-case is if we want to swap out an SQL datastore for a MongoDB datastore, we wont have to change any logic in the PersonResource.

This is how it would look to create a person resource in our main function:

package main

import "github.com/swsmile/golang-wire-di-demo/demo1/my_pkg1"

func main() {
	e := my_pkg1.InitializePersonResource("sw")
	println(e.GetData()) // print get from MongoDataStore
}

Google wire

Basic Usage

Install Wire by running:

go get github.com/google/wire/cmd/wire

and ensuring that $GOPATH/bin is added to your $PATH.

‍ The no framework approach is quite a verbose way of injecting dependencies. The main function can get very long as you can potentially have a lot of dependencies to create for a structure. Google wire is a framework which will generate the dependencies at compile time. This is meant to simulate what a developer would likely write while creating dependencies. Lets change our main package a little:

Lets build an example:

//+build wireinject

package my_pkg1

import (
	"github.com/google/wire"
	"log"
	"os"
)

// Define Injector1
func InitializePersonResource(name string) *PersonResource {
	wire.Build(NewDatastore, NewLogger, NewPersonResource)
	return &PersonResource{Name: name}
}

type MongoDataStore struct{}

func (m *MongoDataStore) GetDataFromStore() string {
	return "get from MongoDataStore"
}

type DataStore interface {
	GetDataFromStore() string
}

type PersonResource struct {
	Name   string
	Store  DataStore
	Logger *log.Logger
}

// Define Provider 1
func NewPersonResource(store DataStore, logger *log.Logger) *PersonResource {
	return &PersonResource{Store: store, Logger: logger}
}

func (a *PersonResource) GetData() string {
	return a.Store.GetDataFromStore()
}

// Define Provider 2
func NewLogger() *log.Logger {
	return log.New(os.Stderr, "exp", log.LstdFlags|log.Lshortfile)
}

// Define Provider 3
func NewDatastore() DataStore {
	return &MongoDataStore{}
}

All we are doing in this file is giving wire the providers needed to create a PersonResource.

If we run wire, it generates a file for us:

// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//+build !wireinject

package my_pkg1

import (
	"log"
	"os"
)

// Injectors from file1.go:

// Define Injector1
func InitializePersonResource(name string) *PersonResource {
	dataStore := NewDatastore()
	logger := NewLogger()
	personResource := NewPersonResource(dataStore, logger)
	return personResource
}

// file1.go:

type MongoDataStore struct{}

func (m *MongoDataStore) GetDataFromStore() string {
	return "get from MongoDataStore"
}

type DataStore interface {
	GetDataFromStore() string
}

type PersonResource struct {
	Name   string
	Store  DataStore
	Logger *log.Logger
}

// Define Provider 1
func NewPersonResource(store DataStore, logger *log.Logger) *PersonResource {
	return &PersonResource{Store: store, Logger: logger}
}

func (a *PersonResource) GetData() string {
	return a.Store.GetDataFromStore()
}

// Define Provider 2
func NewLogger() *log.Logger {
	return log.New(os.Stderr, "exp", log.LstdFlags|log.Lshortfile)
}

// Define Provider 3
func NewDatastore() DataStore {
	return &MongoDataStore{}
}

This file looks very similar to how we created the dependencies without a framework. In this simple example, it doesn’t look very impressive, but if we add more dependencies to datastore and make those also have dependencies, Google wire saves us a lot of time in injecting these dependencies.

Try to run the main file:

package main

import "github.com/swsmile/golang-wire-di-demo/demo1/my_pkg1"

func main() {
	e := my_pkg1.InitializePersonResource("sw")
	println(e.GetData()) // print get from MongoDataStore
}

Advanced Features

The following features all build on top of the concepts of providers and injectors.

Binding Interfaces

Frequently, dependency injection is used to bind a concrete implementation for an interface. Wire matches inputs to outputs via type identity, so the inclination might be to create a provider that returns an interface type. However, this would not be idiomatic, since the Go best practice is to return concrete types. Instead, you can declare an interface binding in a provider set:

type Fooer interface {
    Foo() string
}

type MyFooer string

func (b *MyFooer) Foo() string {
    return string(*b)
}

func provideMyFooer() *MyFooer {
    b := new(MyFooer)
    *b = "Hello, World!"
    return b
}

type Bar string

func provideBar(f Fooer) string {
    // f will be a *MyFooer.
    return f.Foo()
}

var Set = wire.NewSet(
    provideMyFooer,
    wire.Bind(new(Fooer), new(*MyFooer)),
    provideBar)

The first argument to wire.Bind is a pointer to a value of the desired interface type and the second argument is a pointer to a value of the type that implements the interface. Any set that includes an interface binding must also have a provider in the same set that provides the concrete type.

Misc

Mocking

There are two approaches for creating an injected app with mocked dependencies. Examples of both approaches are shown here.

Approach A: Pass mocks to the injector

Create a test-only injector that takes all of the mocks as arguments; the argument types must be the interface types the mocks are mocking. wire.Build can’t include providers for the mocked dependencies without creating conflicts, so if you’re using provider set(s) you will need to define one that doesn’t include the mocked types.

Approach B: Return the mocks from the injector

Create a new struct that includes the app plus all of the dependencies you want to mock. Create a test-only injector that returns this struct, give it providers for the concrete mock types, and use wire.Bind to tell Wire that the concrete mock types should be used to fulfill the appropriate interface.

Reference