di

package
v1.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 29, 2025 License: Apache-2.0 Imports: 5 Imported by: 0

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

Constants

This section is empty.

Variables

This section is empty.

Functions

func Bind

func Bind[T any](
	in *Injector,
	slot Slot[T],
	provider Provider[T],
	resolver Resolver,
)

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

func NewScope(ctx context.Context) context.Context

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

func Optional[T any](in *Injector, slot Slot[T]) T

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

func Override[T any](
	in *Injector,
	slot Slot[T],
	provider Provider[T],
	resolver Resolver,
)

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

func Required[T any](in *Injector, slot Slot[T]) T

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

func Tag(slot any) string

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

func Use[T any](in *Injector, slot Slot[T]) (T, error)

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

func NewInjector(opts ...Option) *Injector

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

func (in *Injector) Context() context.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:

  1. Propagation: It allows for the propagation of request-scoped values, deadlines, and cancellation signals throughout the dependency graph.

  2. 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).

func (*Injector) Resolve

func (in *Injector) Resolve(slot any) (any, error)

Resolve is a non-generic method to resolve a dependency from a slot. In most cases, the type-safe functions (Use, Optional, Required) should be preferred. Resolve is mostly useful for framework integrations that may need to work with slots of an unknown type.

type Option

type Option func(*config)

Option configures an Injector.

func WithContext

func WithContext(ctx context.Context) Option

WithContext sets the root context for the Injector. If ctx is nil, the background context is used by default.

type Provider

type Provider[T any] func(in *Injector) (T, error)

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.

func Singleton

func Singleton() Resolver

Singleton returns a Resolver that creates an instance once per injector and reuses it for all subsequent requests.

func Transient

func Transient() Resolver

Transient returns a Resolver that creates a new instance of the service every time it is requested.

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

func NewSlot[T any](keys ...string) Slot[T]

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL