Dependency injection in Go with Google Wire

Bagus Brahmantya
Towards Dev
Published in
6 min readApr 5, 2024
Photo by John Barkiple on Unsplash

Separation of concerns, loosely coupled system, and dependency inversion principle, among other things, are well known concepts in software engineering and are important in the process of creating good computer program. In this article, we will discuss about a technique that applies these three principles altogether, called dependency injection. We will be practical as much as possible by putting more emphasis on how to implement dependency injection, especially in a Go application. But before going any further, let us revisit the basic question of what exactly dependency injection is?

As mentioned previously, dependency injection is a technique, with its concern being to ensure that an object or function which wants to use a particular service should not have to know how to construct such service, thus separating the concerns of constructing objects and using them, leading to loosely coupled program. The receiving object or function is provided with its dependencies by an external injector, which it is not aware of. Dependency injection also encourages the use of the dependency inversion principle, in which the receiving object only needs to declare the interfaces of the services it uses, rather than their concrete implementations.

Robert van Gen, in one of his articles, stated that while dependency injection works great at small scale, it will result in a big block of initialization code for much larger applications with complex graph of dependencies. This is where tools like Wire will be very helpful. Wire is a code generator for dependency injection in Go. Yes, you guessed it right, Wire will generate the necessary initialization code for us. We only have to define the providers and injectors. Providers are ordinary Go functions that provide values given their dependencies, while injectors are functions that call providers in dependency order. To illustrate this better, an example will be presented.

Setting up the playground

Consider, we are developing HTTP server that provides an endpoint for user registration. Despite having only one endpoint, it is designed with three layers typically appears in a more complex application: Repository, Usecase and Controller. And for not so important reason, let us assume that it has the following directory structure,

.
├── go.mod
├── go.sum
├── internal
│ ├── domain
│ │ ├── model
│ │ │ └── user.go
│ │ └── repository
│ │ └── user.go
│ ├── handler
│ │ └── handler.go
│ ├── interface
│ │ └── datastore
│ │ └── user.go
│ └── usecase
│ ├── request
│ │ └── user.go
│ ├── user
│ │ └── user.go
│ └── user.go
└── main.go

Now, let us define our first provider in internal/interface/datastore/user.go. In the following code snippet, New is a provider function that takes *sql.DB as dependency and returns concrete implementation of Repository.

This concrete implementation of Repository will be used by Usecase layer via an abstraction or interface. In another words, our provider function for Usecase layer depends on an interface rather than concrete implementation of Repository. Technically, this interface should be owned by the consuming layer, but — personally I think — it does not necessarily mean that both of them should live in the same package. In our example, the provider for Usecase and interface for Repository are defined in internal/usecase/user/user.go and internal/domain/repository/user.go, respectively.

Just like the provider for Repository earlier, our provider for Usecase here also returns a concrete implementation.

Finally, the concrete implementation of Usecase will be used by Controller, again via an abstraction or interface. The provider for Controller and interface for Usecase are defined in internal/handler/handler.go and internal/usecase/user.go, as follows

Now that all the necessary providers are complete, we can manually perform dependency injection in our main.go like this

Next, how to use Wire to produce initialization code like above?

Using Wire

With wire, we intend to make our final main.go to look much more simplified like this

We may begin by creating a file, typically named wire.go. It can be defined in a separate package, but in this example we will define it at project’s root directory. But before proceeding to create wire.go, it is better to refactor some portion of our previous code, especially when creating database connection instance and registering API routes. The following new providers will serve this purpose,

The provider function Register above accepts concrete implementation of Handler. Of course, it is possible to use abstraction or interface. But we will leave it as is, just like we let provider function for Repository to accept concrete implementation of type *sql.DB. This does not contradict our Dependency Inversion Principle mentioned earlier. In fact, this is perhaps a good example that we do not have to create abstractions in our code if there is no immediate reason to do so.

Okay, let us go back to our wire.go. According to our simplified main.go file, you might have realized that InitializeHandler function is possibly generated by Wire — yes, you are right! To generate such function correctly, we may write our wire.go as follows,

Basically, in wire.go, we tell Wire about the template of injector function InitializeHandler. It returns *http.ServeMux and error. Notice that the returned values of (&http.ServeMux{}, nil) are there only to satisfy the compiler. And to correctly return the desired value, we declare all the necessary providers to use in Build function: mysql.New, datastore.New, user.New, handler.New and handler.Register.

Although Wire is smart enough to identify the dependency graph, it still needs to be told explicitly that certain concrete implementation satisfies certain interface. Remember that datastore.New and user.New return concrete implementations with type *datastore.Repository and *user.Usecase that satisfy repository.Repository and usecase.Usecase interface, respectively. The necessary explicit declarations for both cases are achieved with Bind function.

Note that we need to exclude wire.go from our final binary. This is done by adding a build constraint at the top of wire.go file.

Next, we may invoke the wire command at the root of our application,

wire

If you have no Wire installed before, please run the following command first,

go install github.com/google/wire/cmd/wire@latest

This wire command will produce a file named wire_gen.go whose content is the generated code for InitializeHandler function, as follows

The generated code for the initializer looks pretty much the same with what we had written in our first version of main.go.

Modifying dependencies

Let us say, we want to modify our mysql.New provider to accept a config struct because we do not want to hard-code the data source name directly in it — a practice that is usually considered bad. To achieve that, we create a special directory to store an application configuration file and a new provider which read the file and returns a config struct. Our final directory structure will look like this,

.
├── config
│ ├── config.go
│ └── file
│ └── config.json
├── go.mod
├── go.sum
├── internal
│ ├── domain
│ │ ├── model
│ │ │ └── user.go
│ │ └── repository
│ │ └── user.go
│ ├── handler
│ │ ├── handler.go
│ │ └── route.go
│ ├── interface
│ │ └── datastore
│ │ └── user.go
│ └── usecase
│ ├── request
│ │ └── user.go
│ ├── user
│ │ └── user.go
│ └── user.go
├── main.go
├── pkg
│ └── mysql
│ └── mysql.go
├── wire_gen.go
└── wire.go

In config/config.go, we define the Config struct as well as its provider,

Next, all we have to do is to add this new provider to our wire.go file. Yes, you are correct, insert it as part of the pipeline of Build function,

Rerun wire command again — or this time we can also run go generate command — will tell Wire to regenerate the initializing code, and the result is as follows

Easy, right?

Final words

We have presented a simple example of using Wire, demonstrating how it may help us to build initializing code with dependency injection. But, this is not the entire story of Wire. In fact, it still has some other useful features not yet discussed here. To gain the maximum advantage of using Wire, please consult the documentation here.

Resources

  1. https://en.wikipedia.org/wiki/Dependency_injection
  2. https://go.dev/blog/wire
  3. Teiva Harsanyi’s “100 Go Mistakes and How to Avoid Them”

--

--

Former electrical engineer turned backend developer, but studied data science as well.