-
Notifications
You must be signed in to change notification settings - Fork 0
Registration
A factory returns exactly one value and takes either nothing or a single
dix.Resolver:
func() T
func(dix.Resolver) TAnything 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) { ... })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 inmain, where the error can abort startup. - If a factory genuinely cannot fail late,
panicin it. The panic surfaces insideResolveon 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})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.
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.
c.Singleton(func() *Cache { ... })
c.Transient(func() *Cache { ... }) // ErrAlreadyRegisteredAllowing 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
Lifetimes → Replacing a registration.
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)
}dix — Dependency Injection eXperience · MIT · © 2026 Kryovyx · pre-1.0, interfaces may change
Concepts
Reference
Practice
Ecosystem