How to Use Wire for Compile-Time Dependency Injection

Wire generates Go code at build time to wire dependencies, requiring the 'wire' CLI tool to run before compilation.

The spaghetti in main

You are building a Go service. You have a UserRepository, a UserService, and an HTTPHandler. The handler needs the service. The service needs the repo. You start wiring them in main. Then you add a Cache. Then a Logger. Then a Database config. main turns into a spaghetti ball of constructors. You change one interface, and five files break. You wish there was a way to describe the graph once and have the computer build it for you.

That is where Wire comes in. Wire is a tool from Google that generates dependency injection code. It does not run at runtime. It does not use reflection. It generates a Go file. You compile that file. The result is a binary with zero reflection overhead for injection. It is a code generator, not a framework.

Think of Wire like a blueprint for a house. You do not live in the blueprint. You use the blueprint to build the house. Once the house is built, the blueprint is gone. Wire is the blueprint. The generated code is the house.

Wire is a code generator

Wire reads your code to understand the dependency graph. You define provider functions that create dependencies. You define injector functions that describe the final output. Wire analyzes the signatures and generates the wiring logic.

The generated code is plain Go. You can read it. You can debug it. If the generation fails, you get an error message from the wire command. If the generated code has a type error, the Go compiler catches it. You get two layers of safety.

Wire generates code you can read. If you cannot read the generated file, you do not understand the dependency graph.

Minimal example

Here is the simplest setup: one provider, one injector, and the generated result.

// wire.go
// +build wire
//go:build wire

//go:generate wire
package main

import "github.com/google/wire"

// NewDB creates a database connection.
// Wire treats this as a provider: it knows how to produce a *DB.
func NewDB() *DB {
    return &DB{Host: "localhost"}
}

// NewApp is the injector.
// Wire generates the implementation of this function based on the providers listed in wire.Build.
// The return type *App tells Wire what the final goal is.
func NewApp() *App {
    wire.Build(NewDB)
    return nil
}

The //go:build wire tag is crucial. It tells the Go compiler to ignore this file during normal builds. Wire reads this file to understand the graph. If you forget the tag, the compiler tries to compile the stub code. You get undefined: wire because the wire package is not a runtime dependency. The build tag keeps the generator metadata out of your binary.

Wire generates wire_gen.go. This file contains the actual wiring logic.

// wire_gen.go
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
package main

// NewApp is a generated function.
// It calls the providers in the correct order and returns the final dependency.
func NewApp() *App {
    db := NewDB()
    app := &App{DB: db}
    return app
}

The generated code calls NewDB, assigns the result to a variable, and constructs the App. It is exactly what you would write by hand. Wire just saves you the typing and ensures the order is correct.

Wire generates code that passes gofmt. You do not argue with the generator. The output is formatted consistently. Most editors run gofmt on save, so the generated file blends in with your codebase.

Wire generates code you can read. If you cannot read the generated file, you do not understand the dependency graph.

How the build works

Wire integrates with go generate. You add //go:generate wire to the top of wire.go. When you run go generate ./..., Go executes the wire command. This is the standard way to run code generators in Go. It ensures everyone uses the same generation step.

The wire command scans the package for wire.go files. It resolves the dependency graph. It writes wire_gen.go. If the graph has cycles or missing providers, generation fails. You get an error message like wire: cycle detected: ... or wire: inject: ... no provider found for .... The error tells you exactly what is wrong. You fix the graph before you run the binary.

Commit wire_gen.go to version control. The generated file is part of your build. If you ignore it, developers need to run wire locally to build. This creates friction. Committing the file ensures the build is reproducible. CI can compile the generated code without installing wire. If the graph breaks, generation fails locally, and the commit fails. This keeps the graph consistent.

Wire catches dependency bugs at generation time. Fix the graph before you run the binary.

Realistic example with interfaces

Real-world apps use interfaces. Wire respects Go conventions. You accept interfaces and return structs. Providers return concrete types. Injectors accept interfaces. Wire matches them up based on types.

Here is a realistic setup: an HTTP handler, a service, and a repository.

// main.go
package main

// UserRepo defines the interface for user storage.
// Wire works best when you accept interfaces and return structs.
type UserRepo interface {
    GetUser(id int) (*User, error)
}

// UserService depends on the repository.
type UserService struct {
    Repo UserRepo
}

// NewUserService creates a service.
// Wire uses this signature to satisfy the UserService dependency.
func NewUserService(repo UserRepo) *UserService {
    return &UserService{Repo: repo}
}

// Handler depends on the service.
type Handler struct {
    Service *UserService
}

// NewHandler creates the HTTP handler.
func NewHandler(service *UserService) *Handler {
    return &Handler{Service: service}
}

// NewPostgresRepo implements the UserRepo interface.
// Wire picks this provider to satisfy the UserRepo interface requirement.
func NewPostgresRepo() UserRepo {
    return &PostgresRepo{DSN: "postgres://..."}
}

The wire file defines the injector.

// wire.go
//go:build wire
//go:generate wire

package main

import "github.com/google/wire"

// NewHandlerInjector generates the handler wiring.
// Wire resolves the graph: Handler -> UserService -> UserRepo -> PostgresRepo.
func NewHandlerInjector() *Handler {
    wire.Build(NewHandler, NewUserService, NewPostgresRepo)
    return nil
}

Wire resolves the graph statically. It looks at the return type of NewPostgresRepo and sees it implements UserRepo. It connects the dots. If you change the interface and forget to update the implementation, generation fails. You get wire: inject: ... no provider found for .... The error tells you exactly what is missing.

Wire handles errors automatically. If a provider returns an error, Wire generates code to check the error and propagate it. The injector returns an error. You do not get a nil pointer. You get the error from the failing provider. This follows the if err != nil pattern. Wire writes the boilerplate for you.

Wire resolves the graph statically. If the types do not match, generation fails. You get a compile error, not a runtime panic.

Pitfalls and errors

Wire detects cycles. If A depends on B and B depends on A, generation fails. You get wire: cycle detected: .... Break the cycle by introducing an interface or restructuring the dependencies.

Wire does not manage lifecycles. It creates values. If you need shutdown hooks, you still need to manage that. Wire gives you the instance. You call Shutdown. Use Wire to build the graph. Use a lifecycle manager to run and stop the services.

Wire does not know about context.Context. You have to pass context manually or via a provider. Functions that take a context should respect cancellation and deadlines. Wire can wire a context if you provide a provider that returns one, but usually you pass context at the call site.

Wire supports wire.Set. A set is a named group of providers. You use sets when you have multiple implementations of an interface. For example, you might have NewPostgresRepo and NewMockRepo. You can create a ProductionSet and a TestSet. In your injector, you reference the set. Wire picks the providers from the set. This makes testing easier. You can swap the set without changing the injector signature.

Wire supports wire.Struct. If you have a config struct with many fields, writing a provider function is tedious. wire.Struct generates a provider that maps fields to providers. You pass the struct type and the fields. Wire generates the function. This reduces boilerplate for configuration objects.

Wire catches dependency bugs at generation time. Fix the graph before you run the binary.

Decision matrix

Use Wire when your dependency graph is complex and manual wiring creates boilerplate that obscures logic. Use manual wiring when the graph is simple: three or fewer dependencies is usually fine to wire by hand. Use a configuration struct when you need to pass the same config to many providers without creating a dependency on the config object itself. Use interfaces for dependencies to keep providers swappable, but return concrete structs from providers to keep the graph explicit. Use wire.Set when you need to swap groups of providers for testing or different environments. Use wire.Struct when you have a large config struct with many fields that need injection.

Wire is a tool for wiring. Do not use it to hide bad architecture. If your graph is a mess, Wire will generate a mess.

Where to go next