skip to content

Wiring Dependencies by Hand

Go services usually build their object graph explicitly in main and pass collaborators as struct fields, with the interface declared by the consumer. Expect to be asked why no container is involved.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

4

How does a Go service struct get its collaborators when main wires the program by hand?

level: juniorimportance: must knowfreq 68%

answer

  1. main builds; the type only receives
  2. nothing reaches outward from inside
  3. lower-case fields, one door in
  4. NewThumbnailer(store, cache) returns the wired value

basics

~20 s

Through its constructor's parameters. main creates each collaborator first and passes them to a function such as NewThumbnailer, which stores them in unexported struct fields, so the type never reaches outside itself for a dependency.

solid answer

~40 s

Go ships no wiring framework, so the graph is built by ordinary function calls in one place. A type declares its collaborators as unexported fields, and a constructor function `NewThumbnailer(store *BlobStore, cache *Cache) *Thumbnailer` takes them as parameters and returns the assembled value. `main` is the composition root: it creates the blob store, then the cache, then the thumbnailer that uses both, in dependency order, and hands the finished thing to whatever serves traffic. Everything below `main` constructs nothing it was not given. The payoff is that the whole graph is readable in one file, a test can build the same type with different collaborators, and the constructor is the single place where "every collaborator is present" is guaranteed — a caller cannot produce a half-populated value that compiles and then nil-panics.

code

go · 17 lines
go
type Thumbnailer struct {
	store  *BlobStore // handed in, never looked up
	cache  *Cache
	maxPix int
}

func NewThumbnailer(store *BlobStore, cache *Cache, maxPix int) *Thumbnailer {
	return &Thumbnailer{store: store, cache: cache, maxPix: maxPix}
}

func (t *Thumbnailer) Shrink(img []byte) ([]byte, error) {
	if small, ok := t.cache.Get(img); ok {
		return small, nil
	}
	// ... uses t.store, never constructs one
	return nil, nil
}

go deeper

for a junior

Be ready to write the three pieces from memory: a struct with lower-case fields, a NewX function taking those fields as parameters, and a main that creates the pieces in order and passes them in.

for a middle

Explain why the fields are unexported and what a caller could do if they were not, and say why a constructor returns a pointer when the value owns a mutex or an open handle.

for a senior

Show how the composition root keeps ownership clear — whoever constructed a collaborator closes it — and how you keep a growing main readable by grouping subsystems into their own constructors.

for a principal

Own the position that Go teams wire by hand rather than adopt machinery, and be able to say what would change your mind: graph size, repetition across many binaries, and the cost of a wiring layer nobody on call can step through.

## The shape A Go program that needs several cooperating pieces builds them with plain code. There is no container, no annotation and no reflection step: a struct declares what it needs, a constructor takes those things as parameters, and `main` calls the constructors in dependency order. ```go type Thumbnailer struct { store *BlobStore cache *Cache maxPix int } func NewThumbnailer(store *BlobStore, cache *Cache, maxPix int) *Thumbnailer { return &Thumbnailer{store: store, cache: cache, maxPix: maxPix} } ``` Three conventions are doing the work here. **Unexported fields.** `store` and `cache` start with a lower-case letter, so no package outside this one can read or write them. That makes the constructor the only door into the type. If the fields were exported, any caller could write `&Thumbnailer{}` — a value that compiles, has nil collaborators, and panics on the first method call. Unexported fields turn "fully wired" from a convention into something the compiler helps you keep. **A `NewX` function returning the concrete type.** Go's convention is `NewThumbnailer` in a package where the type is the main export, or simply `New` in a package named for the type (`thumb.New`). It returns `*Thumbnailer`, not an interface, so callers keep every method the type has. A pointer is the normal return when the value owns things that must not be copied — a mutex, an open file, a connection pool — and the collaborators themselves are usually pointers for the same reason. **`main` as the composition root.** One function, at the top of the program, knows how every piece is connected: ```go store, err := NewBlobStore("/var/lib/thumbs") // ... cache := NewCache(256) thumb := NewThumbnailer(store, cache, 4096) ``` Read top to bottom, that is the entire architecture of the process. Nothing below it constructs a collaborator: the thumbnailer does not build its own store, and the store does not go looking for a cache. Dependencies flow one way, downward, from the place that has the whole picture. ## Why hand-wiring is normal in Go rather than a limitation Engineers arriving from ecosystems with a container often expect one here and are surprised there isn't a standard one. The Go answer is that the wiring is cheap enough to write: it is a few dozen lines that a compiler checks, a debugger steps through, and a reader can follow without knowing a second configuration language. A missing collaborator is a compile error at the call site rather than a startup failure with a stack trace through machinery you did not write. When the graph grows large, the usual first move is not a framework but structure: group related collaborators into their own small constructor (`newStorage(...) (*storage, error)`) so `main` assembles a handful of subsystems instead of forty leaves. ## What the type may and may not do with what it was handed A constructor stores its parameters and validates them; it should not do surprising work. Opening files, dialling networks and reading directories are fine when the constructor's name says so and it can report failure — that is why constructors that touch the outside world return `(*T, error)`. What a constructor should not do is reach around its parameters: no looking a collaborator up from somewhere else, no building one "just this once" inside a method. The moment a method constructs its own collaborator, the type stops being something `main` can assemble differently — in another environment, or in a test. ## The everyday consequences - **Substitution is free.** A test builds `NewThumbnailer` with a store pointed at a temporary directory. No machinery is needed, because construction was always just a function call. - **Lifetime is explicit.** Whoever created the collaborator owns closing it, and that is `main`. A type that was handed a store does not close it; a type that opened its own would have to. - **Order is visible.** You cannot pass a collaborator you have not created yet, so the code itself enforces that the graph is built leaves-first. - **Cycles are loud.** Two types that each need the other cannot both be constructed first. In Go this surfaces immediately, at the wiring site, and forces you to break the cycle (usually by passing a function or splitting a responsibility) rather than hiding it behind lazy resolution. The habit to build is simple: a type receives what it needs, keeps it privately, and someone above it decides what that is.

  • Why keep those collaborator fields unexported instead of letting callers set them after construction?
    Because the constructor is then the only place where "every collaborator is present" is guaranteed. With exported fields, any package can write `&Thumbnailer{}`, get a value that compiles, and hit a nil dereference on the first call. Unexported fields also keep the layout out of your promise to importers, so you can add or rename one later without breaking them.
  • Should NewThumbnailer return a pointer or a value?
    Return `*Thumbnailer` when the value owns things that must not be copied — a mutex, an open file, a connection pool — or when callers must share one instance. A small immutable value type with no such state can be returned by value. The bug to avoid is copying a struct that owns synchronisation or a handle, since the copy silently stops cooperating with the original.
  • Where should the collaborators themselves be created?
    In one composition root — `main`, or a `run` function it calls — so the entire graph is readable in one place. Packages below it construct only their own internal helpers; they never create a collaborator they could have been handed. That is what makes the same types assemblable a different way in a test or in a new environment.

saying these in an interview costs you the question

  • Has the type look its collaborator up from somewhere else
  • Exports every field so callers can populate the struct themselves
  • Expects Go to fill the fields automatically from their types
  • Constructs a fresh collaborator inside each method call
  • Spreads construction across many packages instead of one composition root
open as a page

In a Go main, why should a constructor that opens a dependency return an error instead of calling log.Fatal?

level: middleimportance: should knowfreq 56%

basics

~20 s

Only the program's entry point can decide what a startup failure means. Returning an error lets it stop before the graph is half-built and close what is already open; log.Fatal calls os.Exit, which terminates immediately and skips every deferred cleanup.

open as a page

During shutdown your image service panics writing to a blob store main already closed, so how should main order shutdown?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Close in reverse construction order, and only once the work using a collaborator has stopped: stop accepting requests, drain in-flight handlers with http.Server.Shutdown, then close the blob store. Deferred calls already unwind in that order.

open as a page

In a service's main, how do you decide which collaborators must be reachable at startup and which may start degraded?

level: principalimportance: nice to knowfreq 31%

basics

~20 s

Refuse to start only for collaborators the service cannot keep its contract without: its store, its listener, its configuration. Optional ones such as a cache may start degraded, provided readiness and logs report that truthfully.

open as a page