---
title: "go dependency injection: techniques for clean and testable code"
author: "pilput"
canonical: "https://pilput.net/pilput/go-dependency-injection-techniques-for-clean-and-testable-code"
published: "2026-02-19T04:53:51.243735Z"
updated: "2026-02-19T04:53:51.243735Z"
description: "what is dependency injection and why should you care? if you're writing go code that's more than a simple script, you've likely encountered a common problem: y..."
---
# go dependency injection: techniques for clean and testable code

## what is dependency injection and why should you care?

if you're writing go code that's more than a simple script, you've likely encountered a common problem: your functions and structs become tightly coupled to their dependencies. this makes your code **hard to test, difficult to maintain, and brittle to change**. dependency injection (di) is a design pattern that solves this by decoupling the creation of a dependency from its usage. in simple terms, instead of a struct creating its own database connection or logger, you _inject_ (pass) those dependencies into it from the outside.

for a **full stack** or **devops** engineer, adopting di in your go services leads to cleaner architecture, more reliable unit tests with mocks, and code that's easier to reason about. it's a cornerstone technique for writing production-grade, **coding** best practices.

### the manual approach: constructor injection

the simplest and most idiomatic form of di in go is **constructor injection**. you define a struct that holds its dependencies as fields, and provide a constructor function that accepts those dependencies as arguments.

let's see a classic example without di (the "bad" way):

```
type userservicebad struct {
    db *sql.db
}

func newuserservicebad() *userservicebad {
    // the service creates its own dependency.
    // this is hard to test and inflexible.
    db, _ := sql.open("postgres", "connection_string")
    return &userservicebad{db: db}
}

func (s *userservicebad) getuser(id int) (user, error) {
    // ... use s.db to query
}
```

now, let's refactor it using di:

```
// define an interface for our dependency. this is key for testability.
type userrepository interface {
    getuserbyid(id int) (user, error)
}

type postgresrepository struct {
    db *sql.db
}

func (r *postgresrepository) getuserbyid(id int) (user, error) {
    // ... actual database query
}

// our service now depends on the abstraction (interface), not the concrete implementation.
type userservice struct {
    repo userrepository
}

// constructor injection: dependencies are passed in.
func newuserservice(repo userrepository) *userservice {
    return &userservice{repo: repo}
}

func (s *userservice) getuser(id int) (user, error) {
    return s.repo.getuserbyid(id)
}
```

**why is this better?** in your tests, you can easily pass a _mock_ repository that implements the `userrepository` interface, without needing a real database. this makes your unit tests **fast, isolated, and reliable**.

## managing complex dependency graphs: frameworks

for small projects, manual di is perfect. but as your application grows—with dozens of services, configurations, and shared resources—manually wiring dependencies (creating the "dependency graph") becomes tedious and error-prone. this is where di frameworks come in.

they automate the process of constructing your object graph, managing lifetimes (like singletons), and ensuring all dependencies are satisfied. two popular choices in the go ecosystem are **google wire** and **uber's dig/fx**.

### 1. google wire: compile-time dependency injection

wire is a **code-generation** tool. you write "provider" functions that create your dependencies, and wire generates the `inject` function that wires everything together at compile time.

**pros:** no runtime reflection, type-safe, catches errors at compile time, minimal runtime overhead.
**cons:** requires a separate code generation step (`go generate`).

_example workflow:_

- you define your structs and provider functions (e.g., `newuserservice`, `newpostgresrepo`).

- you create a `wire.go` file with `wire.build(...)` directives listing your final object and its dependencies.

- run `go generate`, which creates a `wire_gen.go` file with a `initializeuserservice()` function that constructs the entire graph.

```
// +build wireinject
package main

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

// this function is generated by wire. you call it to get a fully-wired userservice.
func initializeuserservice(config config) (*userservice, error) {
    wire.build(
        newconfig,          // provider for config (maybe reads env vars)
        newpostgresdb,      // provider for *sql.db
        newpostgresrepository, // provider that depends on *sql.db
        newuserservice,     // provider that depends on userrepository
    )
    return &userservice{}, nil
}
```

### 2. uber fx: runtime dependency injection

fx is a **runtime** di framework built on reflection. you define your application as a set of _modules_ that provide and consume dependencies. fx then builds and runs your application's object graph.

**pros:** extremely flexible, powerful lifecycle hooks (onstart, onstop), great for complex applications with plugins.
**cons:** runtime errors if the graph is misconfigured, slight reflection overhead.

_example structure:_

```
func main() {
    app := fx.new(
        // provide a config struct (a singleton)
        fx.provide(newconfig),
        // provide a database connection. fx will ensure it's created only once.
        fx.provide(newpostgresdb),
        // provide a repository that *depends on* the db.
        fx.provide(newpostgresrepository),
        // provide the final service.
        fx.invoke(newuserservice),
        // hook to start a server after all dependencies are ready.
        fx.invoke(func(s *userservice) { s.startserver() }),
    )
    app.run()
}
```

notice the separation: `fx.provide` registers constructors, `fx.invoke` calls functions (often for side-effects like starting a server) with all their dependencies filled.

## comparison at a glance

    featuremanual digoogle wireuber fx
    **type safety**full (compile-time)full (compile-time, generated code)runtime (can fail at startup)
    **boilerplate**highlow (after generation)very low
    **reflection overhead**nonenoneminimal
    **learning curve**basic (go interfaces)medium (code-gen patterns)steeper (application lifecycle)
    **best for**small projects, librariesmedium/large apps, performance-sensitivelarge, complex microservices/plugins

## best practices for testable go code

regardless of the technique you choose, follow these rules:

- **depend on abstractions (interfaces)**: your structs should depend on interfaces, not concrete implementations. this is the "d" in the solid principles and is **fundamental** for mocking.

- **keep constructors simple**: constructors (`newx`) should do minimal work—just assign fields. complex setup logic can go in an `init` or `start` method, which is easier to test.

- **pass dependencies explicitly**: the dependency chain should be visible in the function signatures. avoid using global variables (`var db *sql.db`) or init functions to set dependencies.

- **scope dependencies appropriately**: is a dependency needed per-request (like a request-scoped logger) or for the entire application lifetime (like a database pool)? frameworks like fx help manage this with `fx.option` and lifecycle annotations.

## conclusion: choose the right tool for your journey

start with **manual constructor injection**. it's the go way and teaches you the core concepts. when wiring becomes a chore, evaluate **google wire** for its simplicity and safety, or **uber fx** if you need its powerful runtime features. the ultimate goal is the same: **clean, modular, and testable code**. by mastering di, you move from just **coding** to architecting resilient go applications that scale in complexity and team size—a critical skill for any **devops** or **full stack** engineer.
