Dependency injection in Go using Google Wire

Estimated read time 17 min read
Dependency injection in Go using Google Wire

Concepts such as separation of concerns, loosely coupled systems, and the dependency inversion principle are well known in software engineering and are crucial in the process of creating good computer programs. In this article, we will discuss a technique that applies all three principles together, called dependency injection. We’ll be as hands-on as possible, focusing more on how to implement dependency injection in Go applications. But before going any further, let’s revisit what exactly is dependency injection?

As mentioned earlier, dependency injection is a technique that focuses on ensuring that an object or function that wants to use a specific service does not have to know how to construct such a service, thereby separating the concerns of constructing the object and using the object, resulting in loose coupling program. The receiving object or function has its dependencies provided by an external injector, of which it is unaware. Dependency injection also encourages the use of the dependency inversion principle, that is, the receiving object only needs to declare the interfaces of the services it uses, without their specific implementation.

Robert van Gen points out in one of his articles that while dependency injection works well on a small scale, for larger applications with complex dependency graphs it will result in large blocks of initialization code. This is where tools like Wire are very useful. Wire  is a code generator for dependency injection in Go. Yes, you guessed it, Wire will generate the necessary initialization code for us. We just need to define the provider and injector. Providers are normal Go functions that provide values ​​based on their dependencies, while injectors are functions that call providers in dependency order. To better illustrate this, an example will be presented.

Set up a playground

Suppose we are developing an HTTP server that provides endpoints for user registration. Although there is only one endpoint, it uses a three-tier design typically found in more complex applications: repository, use case, and controller. For less important reasons, let’s assume 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’s   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 a dependency and returns a concrete implementation of Repository.

// internal/interface/datastore/user.go
package datastore

import (
    "context"
    "database/sql"
    "inject/internal/domain/model"
)

type Repository struct {
    db *sql.DB
}

func New(db *sql.DB) *Repository {
    return &Repository{db: db}
}

func (r Repository) Create(ctx context.Context, user model.User) error {
    // TODO: implement me
    return nil
}

The concrete implementation of this Repository will be used by the UseCase layer through abstractions or interfaces. In other words, the provider functions of our UseCase layer depend on the interface rather than the specific implementation of the Repository. Technically, this interface should be owned by the consumer layer, but – in my personal opinion – that doesn’t mean both of them have to be in the same package. In our example, the Usecase provider and the Repository interface are defined in  internal/usecase/user/user.go  and  internal/domain/repository/user.go respectively  .

// internal/usecase/user/user.go
package user

import (
    "context"
    "inject/internal/domain/repository"
    "inject/internal/usecase/request"
)

type Usecase struct {
    repository repository.Repository
}

func New(repository repository.Repository) *Usecase {
    return &Usecase{repository: repository}
}

func (u Usecase) Create(ctx context.Context, req request.CreateUserRequest) error {
    // TODO: implement me
    return nil
}

Just like the previous provider of Repository, here our UseCase provider also returns a concrete implementation.

// internal/domain/repository/user.go
package repository

import (
    "context"
    "inject/internal/domain/model"
)

type Repository interface {
    Create(ctx context.Context, user model.User) error
}

Finally, the concrete UseCase implementation will be used by the Controller, again through an abstraction or interface. Controller providers and UseCase interfaces  are defined in internal/handler/handler.go  and  internal/usecase/user.go  , as follows:

// internal/interface/datastore/user.go
package handler

import (
    "inject/internal/usecase"
    "net/http"
)

type Handler struct {
    usecase usecase.Usecase
}

func New(usecase usecase.U

secase) *Handler {
    return &Handler{usecase: usecase}
}

func (h Handler) Create() http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // TODO: implement me
        w.WriteHeader(http.StatusOK)
    }
}
// internal/usecase/user.go
package usecase

import (
    "context"
    "inject/internal/usecase/request"
)

type Usecase interface {
    Create(ctx context.Context, req request.CreateUserRequest) error
}

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

// main.go
package main

import (
    "database/sql"
    "log"
    "net/http"

    "inject/internal/handler"
    "inject/internal/interface/datastore"
    "inject/internal/usecase/user"

    _ "github.com/go-sql-driver/mysql"
)

func main() {
    db, err := sql.Open("mysql", "dataSourceName")
    if err != nil {
        log.Fatalf("sql.Open: %v", err)
    }

    repository := datastore.New(db)
    usecase := user.New(repository)
    handler := handler.New(usecase)

    mux := http.NewServeMux()
    mux.HandleFunc("POST /users", handler.Create())

    log.Fatalf("http.ListenAndServe: %v", http.ListenAndServe(":8000", mux))
}

Next, how to use Wire to generate initialization code like the above?

Use Wire

With Wire, we’re going to make our final main.go look more streamlined, like this:

// main.go
package main

import (
    "log"
    "net/http"
)

func main() {
    handler, err := InitializeHandler()
    if err != nil {
        log.Fatal(err)
    }

    log.Fatal(http.ListenAndServe(":8000", handler))
}

We can start by creating a file, usually named wire.go. It can be defined in a separate package, but in this example we will define it in the root of the project. But before continuing to create wire.go, it’s a good idea to refactor some of our previous code, especially the part that created the database connection instance and registered the API route. The following new providers will serve this purpose,

// pkg/mysql/mysql.go
package mysql

import (
    "database/sql"

    _ "github.com/go-sql-driver/mysql"
)

func New() (*sql.DB, error) {
    db, err := sql.Open("mysql", "dataSourceName")
    if err != nil {
        return nil, err
    }
    return db, nil
}
// internal/handler/route.go
package handler

import "net/http"

func Register(handler *Handler) *http.ServeMux {
    mux := http.NewServeMux()
    mux.HandleFunc("POST /users", handler.Create())
    return mux
}

The provider function Register above accepts a concrete implementation of Handler. Of course, abstractions or interfaces can also be used. But we’ll leave it as it is, just as we’ll have the Repository’s provider function accept a concrete implementation of type *sql.DB. This does not violate the dependency inversion principle we mentioned earlier. Actually, this might be a good example where we don’t have to create abstractions in our code if there’s no immediate need for it.

Okay, now let’s go back to our wire.go. Based on our simplified main.go file, you may have realized that the InitializeHandler function was probably generated by Wire — yes, you were right! To correctly generate such a function, we can write our wire.go as follows:

//go:build wireinject
// +build wireinject

package main

import (
    "net/http"
  
    "inject/internal/domain/repository"
    "inject/internal/handler"
    "inject/internal/interface/datastore"
    "inject/internal/usecase"
    "inject/internal/usecase/user"
    "inject/pkg/mysql"

    "github.com/google/wire"
)

func InitializeHandler() (*http.ServeMux, error) {
    wire.Build(
        mysql.New,
        datastore.New,
        wire.Bind(new(repository.Repository), new(*datastore.Repository)),
        user.New,
        wire.Bind(new(usecase.Usecase), new(*user.Usecase)),
        handler.New,
        handler.Register,
    )
    return &http.ServeMux{}, nil
}

Basically, in wire.go, we tell Wire about the template of the initializer function InitializeHandler. It returns *http.ServeMux and error. Note that the return value of (&http.ServeMux{}, nil) is only used to satisfy the compiler. In order to correctly return the required values, we declare all necessary providers in the Build function: mysql.New, datastore.New, user.New, handler.New, and handler.Register.

Although Wire is smart enough to recognize the dependency graph, it still needs to be explicitly told that a specific implementation satisfies a certain interface. Remember, the datastore.New and user.New return types are concrete implementations of *datastore.Repository and *user.Usecase that satisfy the repository.Repository and usecase.Usecase interfaces. The necessary explicit declaration in both cases is achieved through the Bind function.

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

Next, we can call the wire command in the root directory of the application,

wire

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

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

This wire command will generate a file named wire_gen.go, whose content is the generated code of the InitializeHandler function, as shown below:

// Code generated by Wire. DO NOT EDIT.

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

package wire

import (
    "inject/internal/handler"
    "inject/internal/interface/datastore"
    "inject/internal/usecase/user"
    "inject/pkg/mysql"
    "net/http"
)

// Injectors from wire.go:

//go:generate wire
func InitializeHandler() (*http.ServeMux, error) {
    db, err := mysql.New()
    if err != nil {
        return nil, err
    }
    repository := datastore.New(db)


    usecase := user.New(repository)
    handlerHandler := handler.New(usecase)
    serveMux := handler.Register(handlerHandler)
    return serveMux, nil
}

The generated initializer code looks very similar to the code we wrote previously in the first version of main.go.

Modify dependencies

Suppose we want to modify our mysql.New provider to accept a configuration structure because we don’t want to hardcode the data source name directly into it – this is generally considered a bad practice. To achieve this, we create a special directory to store the application configuration files and a new provider that reads the files and returns a configuration structure. 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 structure and its providers,

package config

type Config struct {
    DatabaseDSN string
    AppPort     string
}

func Load() (Config, error) {
    // TODO: implement me
    return Config{}, nil
}

Next, we just need to add this new provider to our wire.go file. Yes, you are right, just insert it as part of the Build function,

//go:build wireinject
// +build wireinject

package wire

import (
    "net/http"

    "inject/config"
    "inject/internal/domain/repository"
    "inject/internal/handler"
    "inject/internal/interface/datastore"
    "inject/internal/usecase"
    "inject/internal/usecase/user"
    "inject/pkg/mysql"

    "github.com/google/wire"
)

func InitializeHandler() (*http.ServeMux, error) {
    wire.Build(
        config.Load,
        mysql.New,
        datastore.New,
        wire.Bind(new(repository.Repository), new(*datastore.Repository)),
        user.New,
        wire.Bind(new(usecase.Usecase), new(*user.Usecase)),
        handler.New,
        handler.Register,
    )
    return &http.ServeMux{}, nil
}

Running the wire command again—or this time we can also run the go generate command—will tell Wire to regenerate the initialization code, with the following results:

// Code generated by Wire. DO NOT EDIT.

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

package wire

import (
    "inject/config"
    "inject/internal/handler"
    "inject/internal/interface/datastore"
    "inject/internal/usecase/user"
    "inject/pkg/mysql"
    "net/http"
)

// Injectors from wire.go:

func InitializeHandler() (*http.ServeMux, error) {
    configConfig, err := config.Load()
    if err != nil {
        return nil, err
    }
    db, err := mysql.New(configConfig)
    if err != nil {
        return nil, err
    }
    repository := datastore.New(db)
    usecase := user.New(repository)
    handlerHandler := handler.New(usecase)
    serveMux := handler.Register(handlerHandler)
    return serveMux, nil
}

Pretty simple, right?

last words

We’ve introduced a simple example using Wire, demonstrating how it can help us build initialization code with dependency injection. But that’s not the whole story of Wire. In fact, it still has some other useful features that haven’t been discussed here yet. To get the most out of Wire, you can refer to the documentation here .

You May Also Like

More From Author

+ There are no comments

Add yours