Skip to content

Registration

wiki edited this page Sep 4, 2026 · 1 revision

Registration

Accepted factory signatures

A factory returns exactly one value and takes either nothing or a single dix.Resolver:

func() T
func(dix.Resolver) T

Anything else is rejected by the registration call itself, with ErrInvalidFactory — not by a panic inside reflect.Call on the first resolve, hours later, in whichever request happened to be first.

// Fine.
c.Singleton(func() *Clock { return systemClock{} })

// Fine — dependencies read through the resolver.
c.Singleton(func(r dix.Resolver) *UserService {
	var repo *UserRepo
	if err := r.Resolve(&repo); err != nil {
		panic(err)
	}
	return &UserService{Repo: repo}
})

// ErrInvalidFactory — two parameters.
c.Singleton(func(a *A, b *B) *C { ... })

// ErrInvalidFactory — two return values.
c.Singleton(func() (*C, error) { ... })

There is no (T, error) factory

Construction cannot report an error. That is a real constraint and worth designing around rather than fighting:

  • Do failable work before registration and register the result with Instance. Opening a database, reading a key file, parsing configuration — all of it belongs in main, where the error can abort startup.
  • If a factory genuinely cannot fail late, panic in it. The panic surfaces inside Resolve on the goroutine that asked, which for a request-scoped factory means one failed request rather than a dead process.
db, err := sql.Open("postgres", dsn)
if err != nil {
	return err // startup fails here, where it should
}
_ = c.Instance(&Database{Pool: db})

Use the resolver you are handed

Inside a factory, resolve through the dix.Resolver parameter — never through a captured container variable:

// Right: `r` is the scope for a Scoped registration.
c.Scoped(func(r dix.Resolver) *UserRepo {
	var tx *Tx
	_ = r.Resolve(&tx)
	return &UserRepo{Tx: tx}
})

// Wrong: `c` is the root, so resolving *Tx returns ErrScopedFromRoot.
c.Scoped(func() *UserRepo {
	var tx *Tx
	_ = c.Resolve(&tx)
	return &UserRepo{Tx: tx}
})

The captured form works right up until one of its dependencies becomes scoped, and then fails everywhere at once.

The return type is the key

Registration is keyed on the factory's declared return type, and Instance on the value's dynamic type. Two consequences:

// Registers *SlogLogger. Resolving Logger finds it by interface matching.
c.Instance(NewSlogLogger())

// Registers the interface type Logger itself, which is usually not
// what you want — it is then the only thing a Logger resolve can match.
c.Singleton(func() Logger { return NewSlogLogger() })

Prefer returning the concrete type and letting interface resolution do the matching. Return an interface only when you deliberately want that interface to be the registered identity.

One registration per type

c.Singleton(func() *Cache { ... })
c.Transient(func() *Cache { ... }) // ErrAlreadyRegistered

Allowing both would force the container to choose between them by some lookup precedence, which makes the lifetime of a dependency depend on an ordering nobody wrote down. Use Unbind to replace deliberately; see LifetimesReplacing a registration.

Errors are returned, not panicked

Every registration method returns error. Ignoring them is a real risk in wiring code, because a rejected registration is invisible until the first resolve fails:

if err := errors.Join(
	c.Singleton(newDatabase),
	c.Scoped(newUnitOfWork),
	c.Transient(newEncoder),
); err != nil {
	return fmt.Errorf("wiring: %w", err)
}

Clone this wiki locally