Documentation
¶
Overview ¶
Package di provides a type-safe, concurrent dependency injection container for Go applications.
The core concepts are:
- Injector: The main container that holds all service bindings.
- Slot: A unique, typed key used to register and retrieve services.
- Provider: A factory function that creates an instance of a service.
- Resolver: A strategy that defines the lifecycle of a service.
Usage ¶
Let's explore how to use this container by modeling a simple chemical reaction: forming a Salt. This example shows how to use a single interface (Ion) for dependencies that fulfill different roles (Cation and Anion).
Step 1: Define a Reusable Abstraction (The Ion Role)
Instead of separate Cation and Anion interfaces, we can define a single, reusable Ion interface. Our Salt struct will now depend on two instances of this same interface type.
// Ion represents any particle with a symbol and a charge.
type Ion interface {
Symbol() string
Charge() int
}
// Salt is our final product, which depends on two Ions.
type Salt struct {
cation Ion
anion Ion
}
func (s Salt) Formula() string {
return s.cation.Symbol() + s.anion.Symbol()
}
Step 2: Create Slots for Roles (The Unique Labels)
The key insight here is that slots distinguish dependencies by their role, not just their type. Even though both slots below are for the Ion type, they are unique keys. This allows us to inject the right ion into the right place. The tags ("ion", "cation", etc.) are optional but help with debugging in case something goes wrong.
var (
SlotCation = di.NewSlot[Ion]("ion", "cation")
SlotAnion = di.NewSlot[Ion]("ion", "anion")
SlotSalt = di.NewSlot[Salt]("compound", "salt")
)
Step 3: Write Providers (The Recipes)
Providers now return the generic Ion interface. The Salt provider can then request two different Ions by using their distinct role-based slots.
// ProvideSodium provides a concrete Ion to fulfill the Cation role.
func ProvideSodium(*di.Injector) (Ion, error) {
type Sodium struct{}
func (na Sodium) Symbol() string { return "Na" }
func (na Sodium) Charge() int { return 1 }
return Sodium{}, nil
}
// ProvideChloride provides a concrete Ion to fulfill the Anion role.
func ProvideChloride(*di.Injector) (Ion, error) {
type Chloride struct{}
func (cl Chloride) Symbol() string { return "Cl" }
func (cl Chloride) Charge() int { return -1 }
return Chloride{}, nil
}
// ProvideSalt requests dependencies by their role-specific slots.
func ProvideSalt(in *di.Injector) (Salt, error) {
// Request the Ion fulfilling the "Cation" role.
cation := di.Required[Ion](in, SlotCation)
// Request the Ion fulfilling the "Anion" role.
anion := di.Required[Ion](in, SlotAnion)
return Salt{cation: cation, anion: anion}, nil
}
Step 4: Assemble the Solution (Configure the Injector)
Now, create an Injector and bind the concrete providers to their respective role slots. We are telling the container that Sodium will act as our Cation and Chloride will act as our Anion.
// 1. Create the injector. solution := di.NewInjector() // 2. Bind concrete providers to their roles. We use Transient scope to // obtain fresh ions each time we form a new salt molecule. di.Bind(solution, SlotCation, ProvideSodium, di.Transient()) di.Bind(solution, SlotAnion, ProvideChloride, di.Transient()) // 3. Bind the provider for the final product. A salt molecule is very // stable, so we treat it as a singleton. di.Bind(solution, SlotSalt, ProvideSalt, di.Singleton())
Step 5: Trigger the Reaction (Resolve the Final Product)
When we ask for the Salt, the injector provides the previously registered atoms (dependencies) to the Salt provider to form the final molecule. As expected, we obtain ordinary table salt (NaCl).
// This call triggers the entire dependency chain.
salt := di.Required[Salt](solution, SlotSalt)
fmt.Printf("Successfully formed: %s\n", salt.Formula())
// Output: Successfully formed: NaCl
Index ¶
- func Bind[T any](in *Injector, slot Slot[T], provider Provider[T], resolver Resolver)
- func NewScope(ctx context.Context) context.Context
- func Optional[T any](in *Injector, slot Slot[T]) T
- func Override[T any](in *Injector, slot Slot[T], provider Provider[T], resolver Resolver)
- func Required[T any](in *Injector, slot Slot[T]) T
- func Tag(slot any) string
- func Use[T any](in *Injector, slot Slot[T]) (T, error)
- type Injector
- type Option
- type Provider
- type Resolver
- type Slot
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Bind ¶
Bind registers a provider and its resolver for a specific service slot. It is typically called during application initialization.
Bind panics if the slot is already bound in the injector.
func NewScope ¶
NewScope creates a new context that carries a cache for scoped dependencies. This should be called at the beginning of an operation that defines a scope, such as a new HTTP request. The returned context should be passed to a new or child injector via WithContext.
func Optional ¶
Optional resolves a service and panics if any resolution error occurs (e.g., an unbound slot or a provider error). However, unlike Required, it allows the provider to return a nil value without panicking. It is useful for dependencies that are truly optional.
func Override ¶
Override registers a provider for a slot, replacing any existing binding. This is primarily useful in testing environments to replace production services with mocks.
func Required ¶
Required resolves a service and panics if an error occurs OR if the resolved value is nil. This should be used for critical dependencies that must always be present.
It checks for nil-ness on interfaces, pointers, maps, slices, channels, and functions.
func Tag ¶
Tag returns the pre-formatted debug string for a slot.
The tag is of the form "name@type", where "name" is the optional name assigned during slot creation, and "type" is the Go type of the slot. If no name was provided, it returns just the type prefixed with "@". If the slot is unknown, it falls back to the pointer address.
func Use ¶
Use resolves a service from the Injector for a given slot. It is the primary method for retrieving dependencies when an error is an expected outcome.
It returns an error if the slot is not bound, if the provider returns an error, or if a circular dependency is detected. If the provider returns a nil value with no error, Use will return the zero value of T.
Use will panic if the value returned by the provider is not assignable to T, which indicates a programming error (e.g., a provider returning an incompatible type).
Types ¶
type Injector ¶
type Injector struct {
// contains filtered or unexported fields
}
Injector is the main dependency injection container. It holds all service bindings and manages their lifecycle. An Injector is safe for concurrent reads (e.g., using Use, Required), but is not safe for concurrent writes (e.g., using Bind, Override). Bindings should be configured once at application startup.
func NewInjector ¶
NewInjector creates and returns a new, empty Injector with the given options. If no options are provided, it defaults to using context.Background().
func (*Injector) Context ¶
Context returns the injector's context.
This context is provided during the injector's creation via the WithContext option. It serves two primary purposes:
Propagation: It allows for the propagation of request-scoped values, deadlines, and cancellation signals throughout the dependency graph.
Scoping: It is the key mechanism for enabling scoped dependencies. Resolvers like Scoped() use this context to cache instances that live for the duration of the context's lifecycle (e.g., an HTTP request).
type Option ¶
type Option func(*config)
Option configures an Injector.
func WithContext ¶
WithContext sets the root context for the Injector. If ctx is nil, the background context is used by default.
type Provider ¶
Provider defines the function signature for a service factory.
When a service is requested, its provider is called with an instance of the Injector, which it can then use to resolve any of its own dependencies (e.g., by calling Use). How often the provider is called depends on the number of injection sites and the resolution strategy used when binding the provider to a slot. By convention, provider functions should be named "Provide<Type>". The associated call to di.Bind should then be done in a function named "Bind<Type>".
type Resolver ¶
type Resolver interface {
// Resolve provides an instance according to the strategy it implements.
// The visiting map tracks the current resolution path to detect cycles.
Resolve(
in *Injector,
provider any,
slot any,
visiting map[any]bool,
) (any, error)
}
Resolver defines a strategy for managing a service's lifecycle.
func Scoped ¶
func Scoped() Resolver
Scoped returns a Resolver that ties the lifecycle of a service to a context.Context. A new instance is created once per scope, defined by a call to NewScope. It requires that the injector's context was created via NewScope.
type Slot ¶
type Slot[T any] *struct{}
Slot is an abstract, typed symbol for an injectable service. It is a unique pointer that acts as a map key within the Injector, while the generic type T provides compile-time type safety.
func NewSlot ¶
NewSlot creates a new, unique Slot for a given type T.
The optional keys are used to create a descriptive name for debugging and error messages. Multiple keys are joined with dots. This is useful to group related services, e.g., by package or feature. The assigned tag can be retrieved later using the Tag function.