Dependency Injection by Hand

Hold your dependencies in a struct, hang handlers off it as methods, and wire it all in main with no container.

Syntaxa := &app{store: store, cfg: cfg}; mux.HandleFunc("GET /x", a.handleX)

A dependency is anything a handler needs but does not create itself: a database connection pool, configuration, a store, a mail sender. Dependency injection just means passing those in from outside instead of reaching for a package-level variable. In other languages this often involves a container, a framework that is told about every type and builds them on request. Go rarely needs one.

The pattern

Define one struct, often called app or api, whose fields are the dependencies. Make each handler a method on *app, so it reads a.store instead of a global. Give the struct a routes method that registers those handlers and returns an http.Handler. Then main is the only function that names concrete types: it builds the store, builds the service, builds the app, and starts the server. mux.HandleFunc(p, a.handleStats) is a method value, shorthand for mux.HandleFunc(p, func(w http.ResponseWriter, r *http.Request) { a.handleStats(w, r) }). Both register the same behaviour, with the receiver a carried along.

Why no container

A container resolves at run time what the compiler already checks at build time. Go interfaces are satisfied implicitly, so a fake store needs no registration anywhere — it only needs the right methods. Hand wiring is a dozen lines you can read top to bottom, and a dependency you forgot to pass fails to compile.

What trips people up

Keep the struct to what handlers actually use. Put cleanup next to construction with defer in main.

Example

Example · go
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
)

// Store is the behaviour the handlers need, not a concrete database.
type Store interface {
	Count() int
}

type memStore struct {
	n int
}

func (m memStore) Count() int {
	return m.n
}

// app holds every dependency the handlers use. No globals, no container.
type app struct {
	store Store
	name  string
}

// handleStats is a method, so it reaches its dependency through the receiver.
func (a *app) handleStats(w http.ResponseWriter, r *http.Request) {
	msg := fmt.Sprintf("%s: %d notes\n", a.name, a.store.Count())
	if _, err := w.Write([]byte(msg)); err != nil {
		return
	}
}

func (a *app) routes() http.Handler {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /stats", a.handleStats)
	return mux
}

func main() {
	// main is the only place that picks concrete types.
	a := &app{store: memStore{n: 3}, name: "notes"}

	rec := httptest.NewRecorder()
	req := httptest.NewRequest(http.MethodGet, "/stats", nil)
	a.routes().ServeHTTP(rec, req)
	fmt.Print(rec.Body.String())
}

// Output:
// notes: 3 notes

When to use it

  • Removing package-level database variables so two tests can run with different data.
  • Giving every handler the same config and store without passing five arguments.
  • Swapping an in-memory store for a database one by editing a single line in main.

More examples

main does the wiring, in order

This is the whole of Go dependency injection: a readable sequence where each value is built from the previous one. Nothing is resolved by magic, so a reader can trace exactly which store the handlers will use. Keep main short — config, construct, serve — and keep every decision about which implementation to use here, so no other package has to care.

Example · go
func main() {
	cfg := Config{Addr: ":8080", DSN: os.Getenv("DATABASE_URL")}

	// Each line needs only what the line above produced.
	store, err := notestore.Open(cfg.DSN)
	if err != nil {
		// log.Fatalf writes the message and exits with status 1.
		log.Fatalf("open store: %v", err)
	}
	// Cleanup sits next to construction, and runs in reverse order.
	defer store.Close()

	svc := note.NewService(store)
	a := &app{notes: svc, cfg: cfg}

	log.Printf("listening on %s", cfg.Addr)
	if err := http.ListenAndServe(cfg.Addr, a.routes()); err != nil {
		log.Fatalf("server stopped: %v", err)
	}
}

A test replaces one field

Because routes() is a method on app, the test exercises the real routing table and the real handler, with only storage swapped. Going through a.routes() rather than calling the handler directly also proves the pattern is registered under the right method and path.

Example · go
func TestStatsHandler(t *testing.T) {
	// No database, no port, no mocking library: one struct literal.
	a := &app{store: memStore{n: 7}, name: "test"}

	rec := httptest.NewRecorder()
	req := httptest.NewRequest(http.MethodGet, "/stats", nil)
	a.routes().ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200", rec.Code)
	}
	if got, want := rec.Body.String(), "test: 7 notes\n"; got != want {
		t.Fatalf("body = %q, want %q", got, want)
	}
}

Method value and its longhand

Knowing these are the same registration matters when reading other people's code, where both appear. The method value is shorter and is the normal choice. The closure form earns its place for per-route settings, which otherwise end up as app fields that most handlers ignore.

Example · go
mux := http.NewServeMux()

// Shorthand: a method value. It carries the receiver a with it, so the
// handler can reach a.store later, when the request arrives.
mux.HandleFunc("GET /stats", a.handleStats)

// Longhand: the same thing written out. Reach for this form when a route
// needs something extra that is not a field on app.
mux.HandleFunc("GET /stats", func(w http.ResponseWriter, r *http.Request) {
	a.handleStats(w, r)
})

// And when a handler needs one extra value, return the closure instead of
// adding a field that only one route uses.
func (a *app) handleLimited(max int) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		a.listNotes(w, r, max)
	}
}

Discussion

  • Be the first to comment on this lesson.