Wire: The Most Elegant Dependency Injection Tool for Go

Introduction

“Mature tools should learn to write code themselves.” This article introduces Wire, a Go dependency injection tool, its usage, and various practical techniques accumulated from experience. When code reaches a certain scale, Wire can play a significant role in component decoupling, development efficiency, and maintainability, especially in large monolithic repositories.

Dependency Injection

As projects grow larger, dependencies in code increase: various database and middleware client connections, data access layer repository instances, and service instances in layered design…

For code maintainability, coupling between components should be avoided. The specific approach follows an important design principle: all dependencies should be passed to components during instance initialization. This is dependency injection (Dependency injection).

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

– Go Official Blog

Here’s a simple example where all component instances Message, Greeter, and Event receive their dependencies during initialization:

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

    event.Start()  
}

Introduction to Wire

When there are more and more component instances in a project, manually writing initialization code and maintaining dependencies between components becomes tedious, especially in large repositories. Therefore, the community has developed several dependency injection frameworks.

Besides Google’s Wire, there are Dig (Uber) and Inject (Facebook). Both Dig and Inject are based on Golang’s Reflection. This not only impacts performance but also makes the dependency injection mechanism opaque to users—very “black box.”

Clear is better than clever, Reflection is never clear.

— Rob Pike

In contrast, Wire is entirely based on code generation. During development, Wire automatically generates component instance initialization code. The generated code is human-readable, can be committed to the repository, and compiles normally. Therefore, Wire’s dependency injection is very transparent and doesn’t bring any performance overhead at runtime.

Getting Started

Here’s how to use 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 wire.go File

Before generating code, we first declare the dependencies and initialization order of each component. Create a wire.go file at the application entry point.

cmd/web/wire.go

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

package main

import "..."  // Simplified example

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

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

This file won’t participate in compilation; it just tells Wire about component dependencies and the expected generation results. In this file: we expect Wire to generate a CreateApp function that returns an App instance or error. All dependencies required for App instance initialization are provided by the ProviderSet provider list, which declares all potentially needed component acquisition/initialization methods and implicitly defines the dependency order between components.

Component acquisition/initialization methods are called “providers” in Wire

Some important notes:

  • The file must start with // +build wireinject and a subsequent blank line, otherwise it will affect compilation
  • In this file, editors and IDEs may not provide code hints, but we’ll show how to solve this problem later
  • The return values in CreateApp (two nils) have no meaning; they’re just for Go syntax compatibility

Step 3: Generate Initialization Code

Execute wire ./... on the command line, and you’ll 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

On-Demand Component Loading

Wire has an elegant feature: no matter how many providers are passed in wire.Build, Wire will only initialize component instances as actually needed. All unnecessary components won’t have corresponding initialization code generated.

Therefore, we can provide as many providers as possible and let Wire handle component selection. This way, whether we’re adding new components or deprecating old ones during development, we don’t need to modify the initialization code in wire.go.

For example, we can provide all instance constructors in the services layer.

pkg/services/wire.go

1
2
3
4
package services

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

In initialization, reference all potentially needed providers.

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 all service instance constructors
	NewApp,
)

func CreateApp() (*App, error) {
	wire.Build(ProviderSet)  // Wire will selectively initialize based on actual needs
	return nil, nil
}

In subsequent development, if you need to reference new dependencies, just add them to the parameters. Wire will intelligently generate initialization code for required component instances based on actual needs.

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

Even if Wire can’t find the required provider, it will report an error during compilation, not during runtime in production.

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

Editor and IDE Configuration Support

Because the wire.go file includes this comment, Go will skip this file during compilation, but this also affects editor and IDE code hints. When editing the wire.go file, common editors and IDEs cannot properly provide code completion and error hints.

1
// +build wireinject

But this problem is easy to solve. Find your IDE/editor’s Go environment configuration and add the parameter -tags=wireinject to Go Build Flags.

This configuration allows editors and IDEs to properly provide code completion and error hints for wire.go files, significantly improving the development experience.

Conflicts with Multiple Instances of the Same Type

This issue is relatively rare, but it’s easy to encounter in large projects.

Wire determines dependencies through provider parameters and return types. Sometimes, different instances of the same type might appear in the dependency network. When this happens, Wire cannot correctly determine dependencies and will report an error directly.

provider has multiple parameters of type ...

For example, in the following provider, the MySQL and PostgreSQL client instances have exactly the same type (both *gorm.DB), so Wire cannot correctly determine dependencies based on type and will report an error during code generation.

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

The solution is relatively simple: just wrap the types.

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

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

Then use ProviderService instead of NewService.

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

Automatic Constructor Generation

As the number of structs serving as abstraction layers increases in a project, manually writing and maintaining struct constructors becomes tedious. If you add a pointer-type member to a struct but forget to update the constructor, it might even cause panics in production.

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

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

Such tedious, repetitive, and error-prone work should be handled by automated tools. I recommend newc (meaning “New Constructor”), which can automatically generate and update struct constructor code.

Usage is very simple; just add this comment to the struct:

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

For example:

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 go generate ./... on the command line to get 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 works great with Wire. When you need to use new dependencies, just add members to the struct. No need to manually update constructors or worry about initialization—all repetitive work is handled by automated tools (Wire and Newc). It works very well in real projects.

Of course, this tool may have some unconsidered situations, and I look forward to everyone’s feedback and suggestions.

Don’t repeat yourself “DRY”

Summary

Wire perfectly solves dependency injection problems, but it’s not a framework—it has no “magic” and isn’t a black box. It’s just a command-line tool that automatically generates initialization code for component instances based on actual needs. Then the problem is solved, with no additional complexity and no runtime performance overhead.

Wire shares the same qualities as Golang: simple, direct, and pragmatic. It truly deserves to be called Go’s most elegant dependency injection tool!

Keep it simple stupid “K.I.S.S”

comments powered by Disqus