Dependency Injection
The di package is the core of the Salusa framework. Dependencies are
registered as factories on a DependencyProvider and are either resolved
directly with Resolve or filled implicitly into structs whose fields carry an
inject tag.
Registering dependencies
Registration happens on a context.Context; the context determines which
DependencyProvider receives the registration. TestDependencyProviderContext
returns a background context carrying a fresh provider, handy in tests and in
init in the generated template:
ctx := di.TestDependencyProviderContext()
di.RegisterSingleton(ctx, func() *Config {
return loadConfig()
})The Register family:
| function | behavior |
|---|---|
Register(ctx, factory) | calls the factory on every resolve |
RegisterWith[T, W](ctx, factory) | fills a W with dependencies, then calls the factory on every resolve |
RegisterSingleton(ctx, factory) | builds once at registration time, returns the same value on every resolve |
RegisterLazySingleton(ctx, factory) | builds at most once, on the first resolve |
RegisterLazySingletonWith[T, W](ctx, factory) | fills a W with dependencies, then builds at most once |
RegisterValue(ctx, t, factory) | registers a factory for a type known only dynamically (reflect.Type) |
NewDependencyProvider() creates an independent provider that can resolve
itself and the surrounding context.Context. Registering the same type twice
replaces the previous factory.
Registering directly on a provider
Every function in the Register family also exists as a method on
DependencyProvider, with the same
behavior and the same type parameters minus the leading ctx. This is the
form to use when you hold a provider directly and do not want to thread a
context through — for example when assembling a provider in a test:
dp := di.NewDependencyProvider()
dp.RegisterSingleton(func() *Config { return loadConfig() })
dp.RegisterLazySingleton(func() (*DB, error) { return openDB() })
dp.RegisterWith(func(ctx context.Context, tag string, deps mailerDeps) (*Mailer, error) {
return NewMailer(deps.Config, deps.Logger)
})| method | behavior |
|---|---|
Register(factory) | calls the factory on every resolve |
RegisterWith[T, W](factory) | fills a W with dependencies, then calls the factory on every resolve |
RegisterSingleton(factory) | builds once at call time, returns the same value on every resolve |
RegisterLazySingleton(factory) | builds at most once, on the first resolve |
RegisterLazySingletonWith[T, W](factory) | fills a W with dependencies, then builds at most once |
RegisterValue(t, factory) | registers a factory for a type known only dynamically (reflect.Type) |
RegisterFactory(factory) | registers an already-built Factory, the escape hatch the others delegate to |
The package-level functions are thin wrappers that pull the provider off ctx
and call the matching method, so di.Register(ctx, f) and
ctx’s provider’s Register(f) are equivalent.
Note The method that accepts a prebuilt
Factoryis namedRegisterFactory. It was previously namedRegisteron*DependencyProvider; a baredp.Register(someFactory)call from older code now needs to becomedp.RegisterFactory(someFactory). Calls that passed a factory function rather than aFactorynever compiled against the old signature, so they are unaffected.
Resolving dependencies
Resolve[T] builds a value of type T from the provider carried by ctx:
cfg, err := di.Resolve[*Config](ctx)If T is a fillable struct, its inject fields are filled recursively instead
of being built by a factory.
Filling structs
A struct whose fields carry an inject tag can be filled with Fill. The first
tag value names the dependency and is passed to the factory, which lets one
factory serve dependencies of the same type under different names. The remaining
values are flags; optional leaves the field as its zero value when the
dependency is not registered:
type handler struct {
Config *Config `inject:""`
DB *sql.DB `inject:"db,optional"`
Cache *redis.Client
}
filled, err := di.Fill(ctx, &handler{})Fields without an inject tag are left untouched. A dependency that is not
registered and is not optional produces an error wrapping
di.ErrNotRegistered.
Dependency-aware factories
RegisterWith and RegisterLazySingletonWith pass a filled struct to the
factory, so a dependency can declare what it needs without resolving anything
itself:
type mailerDeps struct {
Config *Config `inject:""`
Logger *slog.Logger `inject:""`
}
di.RegisterLazySingletonWith(ctx, func(deps mailerDeps) (*Mailer, error) {
return NewMailer(deps.Config, deps.Logger)
})Wrapping functions
PrepareFunc turns a function whose extra parameters are dependencies into a
function with a fixed signature. PrepareFuncCtx is the same, resolving the
extra parameters from the provider carried by the context passed at call
time:
func send(ctx context.Context, user *User) error { ... }
sendUser := di.PrepareFuncCtx[func(ctx context.Context, user *User) error](send)If filling the extra parameters fails and the wrapped function does not return an error, the call panics.
Validation
DependencyProvider.Validate checks the registered factories for missing
dependencies and dependency cycles. Validator returns a DIValidator for a
root type that verifies every inject field has a registered factory:
err := dp.Validate(ctx)ValidatorFor[T](ctx) is the
generic form of Validator, returning a DIValidator for the type T.
A type can declare and validate its own dependencies by embedding
di.Uses[T]. Embedding the zero-size
Uses[T] gives the struct a Validate(ctx) method that reports every inject
field of T that has no registered factory, so a handler can be validated
without a central list:
type Bar struct {
di.Uses[*Foo]
}The kernel calls this on startup so a misconfigured provider fails fast.