Dependency Injection in Go
Simple Dependency Injection in Go
Most components in your application will have dependencies on one or more other components. These components may be external services, such as databases, or they could be other components within your app itself.
Dependency Injection (DI) is a pattern in software engineering in which the dependencies of a particular component are passed in (“injected”) from the outside. This is typically done at the time when the component is constructed.
Here is a simple example of DI in Go:
type Foo struct {
db *sql.DB
}
func NewFoo(db *sql.DB) *Foo {
return &Foo{
db: db,
}
}
func (f *Foo) DoSomething() {
// do something with f.db
}
In this example, the component Foo
interacts with a database. The component’s dependencies are described in the struct
. The dependencies are then provided to the component when an instance of Foo
is created, by passing them into the constructor as an argument. The dependencies are then available for use in the methods the component implements, in this case DoSomething()
.
Why Inject Dependencies?
There are many benefits to this approach to architecting your application.
First and foremost, DI ensures that your components are highly configurable. If Foo
needs to talk to a different database than normal, all you need to do is pass in a different database connection.
An extension of this is that it makes your code highly testable. Instead of passing in a real database connection, you could pass in a mocked database connection instead, (e.g. using sqlmock). Then you can test your component makes the correct queries to the database without requiring a real database to test against.
Indeed, DI even allows you to switch out the behaviour of other, internal application components. For example:
type Fooer interface {
DoSomething()
}
type Bar struct {
foo Fooer
}
func NewBar(foo Fooer) *Bar {
return &Bar{
foo: foo,
}
}
func (b *Bar) DoSomethingElse() {
// do something and call b.foo.DoSomething()
}
Now, a new component Bar
depends on the DoSomething()
behaviour of component Foo
. By using an interface, we can switch out the implementation of Fooer
, allowing us to provide a mocked implementation for testing purposes. More on this in a future post!
Beyond testing, DI makes your code more modular and easier to maintain. Your components do not care about the specific implementation of their dependencies, making them more resilient to design changes. By making dependencies explicit in the code, you are encouraged to think about how each of the pieces in your application are put together. Finally, DI assists in concurrent development whereby a developer can work on a component before the concrete implementation of its dependency is ready.
Complex Dependency Injection
There are cases where the simple pattern described above can become cumbersome in Go. For example, an increasingly long list of dependencies requires that the constructor function accepts an increasingly long list of arguments. Additionally, there are cases where it makes sense for specific subsets of dependencies to be configured together.
A clean solution in this case is to use configuration functions which can be supplied as arguments to the constructor:
type cfgFunc func(*Foo) error
func NewFoo(cfgs ...cfgFunc) (*Foo, error) {
foo := &Foo{}
for _, cfg := range cfgs {
err := cfg(foo)
if err != nil {
return nil, err
}
}
return foo, nil
}
Each config function accepts an instance of Foo
and sets the appropriate dependencies on it, which can be achieved with a closure:
func WithDatabases(db1 *sql.DB, db2 *sql.DB) cfgFunc {
return func(foo *Foo) error {
foo.db1 = db1
foo.db2 = db2
return nil
}
}
Here, WithDatabases
initialises multiple database dependencies on the Foo
instance. A new instance of Foo
can then be created as follows:
foo, err := NewFoo(WithDatabases(db1, db2))
Of course, multiple configuration functions can be provided. In some cases you may even wish to use a hybrid of the simple and complex approaches described here.
Conclusion
In some circles, DI gets a bad rap. Most of the criticism comes from the use of opaque DI frameworks in certain languages. That’s not to say that frameworks don’t exist in Go - there’s Google’s Wire, for example - but I tend to avoid them. What I really like about the approaches described here is their simplicity. No special framework knowledge is required and any developer can read the code and understand it, even if they’ve never heard of DI as a pattern.
What other approaches to DI have you used in Go? Hit me up on Twitter!