At Banzai Cloud we are building a feature rich enterprise-grade application platform, built for containers on top of Kubernetes, called Pipeline - the operating system for your clouds. The platform itself consists of many building blocks - 30+ components - but they share one commonality: they are all developed in Golang.
Dependency Injection is a hot topic in the Go community these days, mainly because of Google’s recently released dependency injection container: Wire. It’s not the first of it’s kind in the Go ecosystem, but, as a product of the Go team, it quickly went viral and started a conversation within the community about whether dependency injection containers are “idiomatic” in Go. While this has not yet grown as heated as the “tabs vs spaces” debate, there are lovers and haters with valid arguments on both sides. These may seem contradictory at first, but, when placed in the proper context, they become easier to understand.
It is, of course, impossible to be completely unbiased in this debate. Everyone has their preferred solution to dependency injection, but it’s important to respect each other’s opinions and preferences. There is no silver bullet or best solution.
Dependency injection 🔗︎
Contrary to what many believe, dependency injection itself does not require a “container” to work. It’s a form of inversion of control, but is probably better known as a solution to the dependency inversion principle which is the letter “D” in SOLID. Both principles touch on writing well-architected, modularized programs.
Dependency injection helps through modularizationm, by injecting dependencies into dependents (clients) instead of letting them ask for or create dependencies themselves.
A simple dependency injection in Go looks like this:
1 db, _ := sql.Open("mysql", "<dsn>")
2
3 repository := NewRepository(db)
In this case, the repository, instead of creating the dependency itself, receives a database connection through a “constructor” function. This is called a constructor injection and it’s probably the purest form of dependency injection.
We don’t have far to go in order to meet other types of dependency injection in Go.
In fact, we can find most of them in the net/http
package.
Specifically, the http.Server
implementation provides two servicable examples.
One is the so-called property injection:
1 server := &http.Server{
2 Handler: http.NewServeMux(),
3 }
In this case, the dependency is the HTTP handler, which is passed to a struct member field (“property”). In a language like Go, this is usually not the safest dependency injection, because there is nothing that prevents concurrent access or subsequent writes, both of which can lead to unexpected behavior. In languages like C#, class members/properties can be made write-once/read-only which makes this kind of injection safer.
The other type of injection in the http.Server
struct is method injection:
1 ln, _ := net.Listen("tcp", ":8080")
2
3 server.Sever(ln)
The dependency here is the listener which, again, instead of being created by the server itself (unlike in ListenAndServe
) is
passed via a method argument.
We’ve already seen a lot of dependency injections, but there’s been nothing about “containers” so far. That’s because containers are not fundamentally part of the dependency injection technique. It comes from the generic term “injector”, which (surprise surprise) is anything responsible for an injection, but each of the above qualifies as an injector, and there’s no obvious requirement for something more sophisticated.
DIC 🔗︎
A dependency injection container (or DIC) hides all those injection codes behind a single interface. It handles the creation of objects and stores those objects for later reuse, hence the name “container”.
A very simple, manually wired container might look like this:
1import (
2 "database/sql"
3)
4
5type myRepository struct {
6 db *sql.DB
7}
8
9type container struct {
10 db *sql.DB
11
12 myRepository *myRepository
13}
14
15func (c *container) getDB() (*sql.DB, error) {
16 if c.db == nil {
17 db, err := sql.Open("mysql", "dsn")
18 if err != nil {
19 return nil, err
20 }
21
22 c.db = db
23 }
24
25 return c.db, nil
26}
27
28func (c *container) getMyRepository() (*myRepository, error) {
29 if c.myRepository == nil {
30 db, err := c.getDB()
31 if err != nil {
32 return nil, err
33 }
34
35 c.myRepository = &myRepository{db}
36 }
37
38
39 return c.myRepository, nil
40}
Now, a perfectly valid question would be, ‘Why on Earth would anyone implement a DIC like this? It’s even more code intensive than manual wiring.’’
Of course no one writes containers this way. The code above is illustrative, and demonstrates the behavior of a DIC, how it does the same things as manual wiring under the hood (so our net gain is a measly few lines of code). An actual DIC would do all these things automatically or semi-automatically with a little bit of help from a developer (e.g. a configuration to let the DIC know how to create or inject dependencies into our code).
So why did we start to use DICs in the first place? Based on what we’ve seen so far, they don’t add much value.
The clearest and most concise answer is probably: frameworks.
As general purpose frameworks evolved, they became more and more complex in order to provide all the features users wanted them to support. Not only that, but frameworks also had to be extensible, so lots of extension points were built into them.
For these reasons, frameworks grew extremely complex, which resulted in equally complex dependency graphs.
And what do we do with complex things? Well, we put them together in one place and hide them, helping to maintain the illusion that the rest of our application is nice and clean.
Ultimately, this is the reason we use DICs. But make no mistake: this can be a perfectly valid reason. Frameworks, with all their structural complexities under the hood, often provide simple tools that help us rapidly create prototypes or, for example, simple CRUD interfaces. Sometimes they’re simply the right tools for the job.
DICs in Go 🔗︎
As we mentioned earlier, Wire was not the first DIC implemented in Go, but it does take an entirely new approach. Implementations like Facebook’s inject or Uber’s dig use reflection to build dependency runtimes. The problem with these is that we only get notified about issues that occur on application startup.
To overcome this, Wire approaches the problem from a new angle: instead of building the dependencies and a dependency graph, at runtime, Wire generates the container (literally, its code is generated) and informs us about any issues at time of compilation.
This sounds great at first, but if we look at the code examples we see that it doesn’t really save any code (we still have to write the same factory functions), and it even adds an extra step in our build pipeline.
Regardless of whether you want to use it, it’s worth checking out Wire.
Go without DICs 🔗︎
One of the main arguments for using DIC in Go is that, “Manually wiring up dependencies is tedious and slow
.”
This is an argument with strongly opinionated supporters, which doesn’t seem to be going away anytime soon. On the other hand, members of the DIC opposition make
the argument that doing things manually can actually be idiomatic in Go, as long as it’s simpler and easier to understand.
While more seasoned Go developers tend to agree on this, it also seems like a matter of personal preference.
Compared to how DICs appear in frameworks, we can probably say that those use cases present themselves considerably less often in the Go ecosystem; there aren’t that many in large frameworks in Go as there are in Java or in C#, for example.
One thing’s for sure: Go has certainly worked fine without injection containers so far, and it will continue to in the forseeable future.
Conclusion 🔗︎
Dependency injection containers have proved to be useful and even though Wire seems like the most promising DIC solution in Go, right now, it does not appear to add significant value in most Go use cases. It’s also important to remember that dependency injections do not require a container. Surprisingly, many people tend to forget about this, maybe because they get used to frameworks first.
In any case, this should be a team decision, made on a per project basis.
Personally, I find the current situation a bit precarious. The fact that the Go team released a dependency injection container might make people think that Wire (and similar tools) represent the “idiomatic dependency injection” in Go, which is obviously untrue.
Further reading 🔗︎
https://blog.golang.org/wire
https://github.com/google/go-cloud/tree/master/wire
https://www.youtube.com/watch?v=LDGKQY8WJEM
https://www.reddit.com/r/golang/comments/9ofrjl/dependency_injection_wire/
https://www.reddit.com/r/golang/comments/9mqctx/compiletime_dependency_injection_with_go_clouds/
https://www.reddit.com/r/golang/comments/91rvzj/wire_a_di_container_from_google/
https://blog.drewolson.org/dependency-injection-in-go