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
- https://github.com/google/wire
- https://github.com/google/wire/blob/main/docs/guide.md
- https://github.com/google/wire/blob/main/_tutorial/README.md
- https://github.com/google/wire/blob/main/docs/best-practices.md
- https://www.nerd.vision/post/dependency-injection-in-go