Wire: The Most Elegant Dependency Injection Tool for Go

Introduction

This article introduces the Go dependency injection tool Wire, its usage, and some practical tips accumulated in practice.

When the codebase grows, Wire plays a significant role in decoupling objects, enhancing development efficiency, and maintainability, especially in large-scale projects.

Dependency Injection

As codebase grow, the number of objects in the code increases: so many database connections, middleware instances, repositories and service instances in layered designs, etc.

To keep code maintainable, we should avoid tightly coupling objects. Instead, we should pass all dependencies to an object when we create it. This approach is called dependency injection.

Dependency injection is a standard technique for producing flexible and loosely coupled code, by explicitly providing objects with all of the dependencies they need to work.

– Go Official Blog

Here’s a simple example where all objects Message, Greeter, and Event receive their dependencies at initialization.

1
2
3
4
5
6
7
func main() {  
    message := NewMessage()  
    greeter := NewGreeter(message)  
    event := NewEvent(greeter)  

    event.Start()  
}

Introduction to Wire

As the number of object dependencies in a project increases, manually writing initialization code and maintaining dependency relationships between objects can become very tedious, especially in large repositories. Hence, there are several dependency injection frameworks in the community.

Besides Google’s Wire, Uber’s Dig and Facebook’s Inject are other dependency injection tools. However, Dig and Inject use Go’s reflection, which can slow things down and make it hard to understand how they work under the hood.

Clear is better than clever, Reflection is never clear.

— Rob Pike

Wire relies solely on code generation. It automatically creates readable and compilable initialization code for objects during development. This makes Wire’s dependency injection process very clear and adds no runtime performance costs.

Getting Started with Wire

Here’s how to start using Wire:

Step 1: Download and Install Wire

Download and install the Wire command-line tool:

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

Step 2: Create a wire.go File

Before generating code, we first define the dependency relationships and initialization order of objects in a wire.go file at the application’s entry point.

cmd/web/wire.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// +build wireinject

package main

import (
	"github.com/google/wire"
    // ...
)

var ProviderSet = wire.NewSet(
	configs.Get,
	databases.NewClient,
	repositories.NewUser,
	services.NewUser,
	NewApp,
)

func CreateApp() (*App, error) {
	wire.Build(ProviderSet)
	return nil, nil
}

This file is not part of the compilation but tells Wire the dependency relationships between objects and the expected generation results. In this file, we expect Wire to generate a CreateApp function that returns an App instance or an error, with all necessary dependencies provided by the ProviderSet list of objects, which also implies the order of dependencies between objects.

Step 3: Generate Initialization Code

Run the command wire ./... in the terminal, and you will get the following automatically generated code file.

cmd/web/wire_gen.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Code generated by Wire. DO NOT EDIT.

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

package main

import "..."  // Simplified example

func CreateApp() (*App, error) {
	conf, err := configs.Get()
	if err != nil {
		return nil, err
	}
	db, err := databases.New(conf)
	if err != nil {
		return nil, err
	}
	userRepo, err := repositories.NewUser(db)
	if err != nil {
		return nil, err
	}
	userSvc, err := services.NewUser(userRepo)
	if err != nil {
		return nil, err
	}
	app, err := NewApp(userSvc)
	if err != nil {
		return nil, err
	}
	return app, nil
}

Step 4: Use the Initialization Code

Wire has generated the real CreateApp initialization method for us, and now we can use it directly.

cmd/web/main.go

1
2
3
4
5
// main.go
func main() {
	app := CreateApp()
	app.Run()
}

Usage Tips

Load Objects as Needed

One elegant feature of Wire is that no matter how many object providers are passed into wire.Build, Wire will always initialize only the necessary objects, and all unnecessary objects will not generate corresponding initialization code.

Therefore, when using it, we can provide as many providers as possible, leaving the selection of objects to Wire. This way, whether we are adding new objects or phasing out old ones during development, we do not need to modify the initialization steps in the wire.go file.

For example, you can provide constructors for all instances in the services layer.

pkg/services/wire.go

1
2
3
4
package services

// Provides constructors for all service instances
var ProviderSet = wire.NewSet(NewUserService, NewFeedService, NewSearchService, NewBannerService)

In initialization, reference as many potentially needed object providers as possible.

cmd/web/wire.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var ProviderSet = wire.NewSet(
	configs.ProviderSet,
	databases.ProviderSet,
	repositories.ProviderSet,
	services.ProviderSet,  // References constructors for all service instances
	NewApp,
)

func CreateApp() (*App, error) {
	wire.Build(ProviderSet)  // Wire will selectively initialize as needed
	return nil, nil
}

In subsequent development, if you need to reference new objects, just add them to the parameters. Wire will diligently generate the necessary object initialization code as needed.

1
2
func NewApp(user *UserService, banner *BannerService) {
}

Even if Wire cannot find a provider for an object, it will report an error during the compilation stage, preventing issues during runtime.

wire: cmd/api/wire.go:23:1: inject CreateApp: no provider found for *io.WriteCloser

Editor and IDE Assistance Configuration

Because the wire.go file includes this comment, Go will skip this file during compilation, but this also affects code completion and error highlighting in editors and IDEs. When you are editing the wire.go file, common editors and IDEs cannot normally provide code completion and error highlighting.

1
// +build wireinject

However, this issue is easy to resolve. Find the Go environment configuration in your IDE/editor and add the -tags=wireinject parameter to the Go Build Flags.

This configuration allows the editor and IDE to normally provide code completion and error highlighting for the wire.go file, significantly improving the development experience.

Resolving Conflicts with Multiple Objects of the Same Type

This issue is relatively rare, but it can occur in large projects.

Wire determines the dependency relationships between objects based on the parameters and return types of providers. Sometimes, the dependency network may contain different objects of the same type, causing Wire to be unable to correctly determine the dependency relationships and directly report an error.

provider has multiple parameters of type ...

For example, the following provider depends on MySQL and PostgreSQL client instances of the same type (*gorm.DB), making it impossible for Wire to correctly determine the dependency relationships based on type, and it will directly report an error when generating code.

1
2
3
// This service uses data from both mysql and pg, but the two objects are of the same type
func NewService(mysql *gorm.DB, pg *gorm.DB) *Service {
}

The solution is relatively simple: just use a type alias for differentiation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type Mysql gorm.DB
type Pg    gorm.DB

// Use type aliases in the parameters for differentiation
func ProviderSerivce(mysql *Mysql, pg *Pg) *Service {
	// Convert back to the original type within the function
	r1 := (*gorm.DB)(mysql)
	r2 := (*gorm.DB)(pg)
	return NewService(r1, r2)
}

Then replace NewService with ProviderSerivce.

1
2
3
4
5
wire.Build(
	ProviderMysql,   // func() *Mysql
	ProviderPg,      // func() *Pg
	ProviderSerivce, // func(mysql *Mysql, pg *Pg) *Service
)

Automatically Generating Constructors

As the number of abstract classes in the project increases, manually writing and maintaining constructors for these classes becomes very tedious. If a pointer-type member is added to a class and the constructor is not updated, it can even cause a panic in production.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
type Service struct {
	repo   *Repository
	logger *zap.Logger  // Forgot to update the constructor after adding this member
}

func NewService(repo *Repository) *Service {
	// Missing logger, may cause a null pointer error in production
	return &Service {
		repo:   repo,
	}
}

Such tedious, repetitive, and error-prone tasks should be handled by automation tools. Here, I recommend an automation tool newc (stands for “New Constructor”), which can automatically generate and update constructor code for classes.

The usage is very simple, just add this comment to the class.

//go:generate go run github.com/Bin-Huang/newc@v0.8.3

For example, like this:

1
2
3
4
5
6
7
// My User Service
//go:generate go run github.com/Bin-Huang/newc@v0.8.3
type UserService struct {
	baseService
	userRepository *repositories.UserRepository
	proRepository  *repositories.ProRepository
}

Then execute the command go generate ./... in the terminal to obtain the constructor code:

constructor_gen.go

1
2
3
4
5
6
7
8
// NewUserService Create a new UserService
func NewUserService(baseService baseService, userRepository *repositories.UserRepository, proRepository *repositories.ProRepository) *UserService {
	return &UserService{
		baseService:    baseService,
		userRepository: userRepository,
		proRepository:  proRepository,
	}
}

This tool, when used in conjunction with Wire, provides an excellent development experience. To use a new object, simply add a member to the class. There is no need to manually update the constructor or worry about initialization issues, as all repetitive tasks are handled by automation tools (Wire and Newc). Colleagues who have tried it have all praised its effectiveness.

Of course, there may be situations that this tool does not adequately address, and I look forward to your feedback and suggestions.

Don’t repeat yourself

“DRY”

Conclusion

Wire perfectly solves the problem of dependency injection, but it is not a framework. It has no “magic” and is not a black box. It is just a command-line tool that automatically generates the initialization code for various objects as needed. Then the problem is solved, without any additional complexity or performance overhead during runtime.

Wire and Golang share the same ethos: simplicity, directness, and pragmatism, making it truly the most elegant dependency injection tool for Go!

Keep it simple stupid

“K.I.S.S”

comments powered by Disqus