How to Use Google Wire for Dependency Injection in Go

Google Wire is a code generation tool for dependency injection in Go, not a standard library, requiring installation via go install and configuration in a wire.go file.

The wiring puzzle

You start a Go project with a single main.go. You need a database connection, a logger, and an HTTP router. You write three constructor calls, pass the results around, and it works. Six months later, the project has grown. You added a cache client, a metrics exporter, and a background worker. Every new feature requires threading five dependencies through constructors. Tests need mock versions of everything. You spend more time assembling objects together than writing business logic. Dependency injection solves the threading problem. Google Wire solves the assembly problem.

What Wire actually does

Dependency injection means handing dependencies to a component instead of letting the component create them. Wire takes that idea and removes the manual assembly code. It is not a runtime framework. It does not use reflection to stitch objects together while your program runs. Wire is a compile-time code generator. You describe what your application needs and how to build each piece. Wire reads that description, draws a dependency graph, and writes plain Go code that constructs everything in the correct order. The generated code lives in your repository alongside your source files. It compiles like any other Go package. If the graph has a cycle or a missing type, the build fails before you ever run the binary.

Wire aligns with Go's preference for explicit over implicit. You see exactly which function creates which value. There are no hidden containers. There are no global registries. The tool writes the boilerplate so you do not have to. Trust the generator. Read the output. Commit it.

A minimal setup

Start with a package that defines a simple service. The service needs a logger and a database connection. You write a provider function for each dependency and an injector function that returns the service.

Here is the marker file that tells Wire how to assemble the pieces.

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

// This file is ignored by the Go compiler during normal builds.
// It only exists as input for the wire command.
package main

import "github.com/google/wire"

// LoggerProvider creates a new logger instance.
// Provider functions take zero or more dependencies and return a value.
func LoggerProvider() *Logger {
    return &Logger{prefix: "app"}
}

// DBProvider creates a database connection.
// It depends on the logger to record connection attempts.
func DBProvider(log *Logger) *DB {
    return &DB{log: log}
}

// ServiceProvider creates the main service.
// It depends on both the logger and the database.
func ServiceProvider(log *Logger, db *DB) *Service {
    return &Service{log: log, db: db}
}

// NewService wires everything together.
// wire.Build tells Wire which providers to use when constructing the return type.
func NewService() *Service {
    wire.Build(LoggerProvider, DBProvider, ServiceProvider)
    return nil
}

Run wire in the directory. The tool generates wire_gen.go. The generated file contains the exact sequence of calls you would have written by hand. It passes the logger to the database provider, passes both to the service provider, and returns the result. The compiler treats wire_gen.go like any other source file. You can open it, read the call order, and verify the types. Keep your provider functions small and focused. Wire handles composition, not business logic.

How the generator resolves your graph

The wire command scans your package for files named wire.go. It looks for functions that call wire.Build. Each wire.Build call lists provider functions. Wire traces the return types and parameter types of those functions to build a directed acyclic graph. It finds the leaf nodes, which are providers with no dependencies. It resolves them first. It works upward until it reaches the injector's return type. Once the graph is complete, it writes a Go function that matches your injector signature.

The generated function calls each provider exactly once. It passes the correct arguments. It returns the final value. Because the output is plain Go, you can debug it with a standard debugger. There is no hidden magic at runtime. If you change a provider signature, you must run wire again. The old generated code will still compile until you touch a file that forces a rebuild, but the types will mismatch. Run wire after every signature change. Most teams add a pre-commit hook or a make generate step to automate this. Wire writes the boilerplate so you do not have to. Trust the generator. Read the output. Commit it.

Realistic application wiring

Real applications rarely have flat dependency trees. You often need to group related providers or inject multiple values at once. Wire handles this with wire.NewSet. Consider an HTTP server that needs configuration, a database, a service layer, and a router.

Here is how you group providers to keep the injector readable.

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

package main

import "github.com/google/wire"

// ConfigProvider reads environment variables and returns a config struct.
func ConfigProvider() *Config {
    return &Config{Port: 8080, DBHost: "localhost"}
}

// DBProvider connects to the database using the config.
func DBProvider(cfg *Config) *DB {
    return &DB{host: cfg.DBHost}
}

// ServiceProvider creates the business logic layer.
func ServiceProvider(db *DB) *Service {
    return &Service{db: db}
}

// RouterProvider builds the HTTP router and attaches handlers.
func RouterProvider(svc *Service) *Router {
    return &Router{svc: svc}
}

// ServerSet groups providers that are always used together.
// This keeps the Build call readable when you have many dependencies.
var ServerSet = wire.NewSet(
    ConfigProvider,
    DBProvider,
    ServiceProvider,
    RouterProvider,
)

// NewServer wires the entire application stack.
func NewServer() *Router {
    wire.Build(ServerSet)
    return nil
}

Run wire again. The generated wire_gen.go will call ConfigProvider, pass the result to DBProvider, pass that to ServiceProvider, and finally pass the service to RouterProvider. The chain is explicit. If you add a new dependency later, you update the provider list and regenerate. The compiler catches missing types immediately. Keep your provider functions pure when possible. Side effects like network calls belong in the provider, but keep them fast. Wire runs once at startup. Do not block the graph resolution with heavy initialization.

Partial injection with wire.In and wire.Out

Sometimes a component only needs a subset of the available dependencies. Wiring every single value into every provider creates noise. Wire solves this with wire.In and wire.Out. You define a struct that lists only the fields you need. Wire fills those fields automatically.

Here is how you request only the database and logger without pulling in the entire config.

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

package main

import "github.com/google/wire"

// ServiceDeps lists exactly what the service layer requires.
// Wire matches fields by type, not by name.
type ServiceDeps struct {
    wire.In
    Log *Logger
    DB  *DB
}

// ServiceProvider creates the business logic layer.
// It accepts the dependency struct instead of individual parameters.
func ServiceProvider(deps ServiceDeps) *Service {
    return &Service{log: deps.Log, db: deps.DB}
}

// NewService wires the partial dependency set.
func NewService() *Service {
    wire.Build(LoggerProvider, DBProvider, ServiceProvider)
    return nil
}

Wire scans the ServiceDeps struct, finds the *Logger and *DB fields, and locates providers that return those types. It ignores everything else. This pattern keeps provider signatures clean when you have dozens of available dependencies. You can also use wire.Out to return multiple values from a single provider. Wire unpacks the struct and routes each field to the correct consumer. Use wire.In when a component has optional dependencies. Mark the field as a pointer and add wire.Optional. Wire will pass nil if no provider exists. Handle the nil case explicitly in your code. Go convention favors explicit over implicit. Wire aligns with that philosophy. The generated code uses standard function calls. There are no hidden containers.

Pitfalls and compile-time failures

Wire shifts dependency errors from runtime panics to compile-time failures. That is the point. You will see errors like wire: inject: missing provider for type "main.DB" if you forget to list a provider in wire.Build. You will see wire: inject: cycle detected: A -> B -> A if two providers depend on each other. Circular dependencies break the graph. Wire cannot resolve them. Break the cycle by extracting a shared interface or moving one dependency into a parent struct.

Another common mistake is forgetting to regenerate after changing a provider signature. The generated code will still compile until you touch a file that forces a rebuild, but the types will mismatch. Run wire after every signature change. Most teams add a pre-commit hook or a make generate step to automate this.

The //go:build wireonly tag is required on wire.go. Without it, go build will complain about duplicate function definitions when wire_gen.go exists. The tag tells the compiler to ignore the marker file during normal builds. Keep the tag. It is part of the standard Wire workflow.

Error handling in providers follows the standard if err != nil { return nil, err } pattern. Wire supports multi-return providers. If a provider returns (T, error), Wire passes the error up the chain and stops generation if the injector expects a clean return. Match your error handling to the rest of your codebase. Context always goes as the first parameter, conventionally named ctx. Functions that take a context should respect cancellation and deadlines. Wire does not care about context, but your providers should. Pass ctx through the graph if your dependencies need it. The worst dependency bug is the one that silently returns a nil value. Wire forces you to face missing types at build time. Face them early.

When to reach for Wire

Use Wire when your application has more than five interdependent components and manual construction is becoming error-prone. Use Wire when you want compile-time guarantees that every dependency is provided and correctly typed. Use Wire when your team prefers explicit, readable construction code over reflection-based runtime injection. Skip Wire when your project is a small CLI tool or a single-file script: the setup overhead outweighs the benefits. Skip Wire when you need dynamic plugin loading or runtime dependency swapping: Wire generates static code that runs once at startup. Reach for manual wiring when you are prototyping and want to iterate quickly without managing a generation step. Reach for a reflection-based container when you need to register dependencies at runtime based on user configuration. Wire writes the boilerplate so you do not have to. Trust the generator. Read the output. Commit it.

Where to go next