Documentation
¶
Overview ¶
Package di is a dependency-injection container for Go 1.27+ built on generic methods.
Services are registered on a Scope and resolved from it by type:
app := di.New()
app.Value(Config{DSN: "postgres://localhost/app"})
app.Provide(func(s *di.Scope) *DB { return s.Must(sql.Open("postgres", s.Get[Config]().DSN)) }).
OnStop(func(ctx context.Context, db *DB) error { return db.Close() })
app.Provide(func(s *di.Scope) *Repo { return &Repo{db: s.Get[*DB]()} })
repo, err := app.Resolve[*Repo]()
Keys are Go types, so there is no naming scheme and no collisions between packages. Constructors return T rather than (T, error): inside a constructor, Scope.Get and Scope.Must abort on failure and the error surfaces from the enclosing Scope.Resolve, Scope.Start or Scope.Run with the dependency path and the registration site.
Lifetimes ¶
A binding is a singleton by default, cached in the scope that registered it. Binding.Scoped makes it one instance per resolving scope, built there so it can see that scope's values, which is how request-scoped services are declared once in the root. Binding.Group and Scope.All handle groups, and Scope.Maybe resolves optional dependencies. An interface is served by a constructor that returns the implementation: s.Provide(func(s *Scope) Reader { return s.Get[*Repo]() }).
Scopes ¶
Scope.Child creates a scope that resolves through its parent, reuses the parent's singletons and owns the lifecycle of what it builds. The last registration of a key wins, which is the test seam: wire the production graph into a fresh scope, then re-register what you want faked before anything is resolved (Test does the bookkeeping). For HTTP, github.com/floatdrop/di/dihttp.Middleware gives each request a child scope holding the *http.Request, reachable through FromContext.
Lifecycle ¶
Binding.OnStart, Binding.OnDrain and Binding.OnStop are typed hooks. Scope.Start builds Binding.Eager bindings and runs start hooks in build order, rolling back on failure; services built later start as part of being built. Scope.Stop first drains, which lets work already in flight finish while the scope still resolves, then stops child scopes, then services in reverse build order, and afterwards the scope refuses to resolve anything. Binding.Worker runs a long-lived function that is cancelled on stop. Scope.Run ties it together for a main function: start, wait for a signal or Scope.Shutdown, stop with a deadline. Scope.Observe reports every step for logging and metrics.
Concurrency ¶
A Scope is safe to use from many goroutines, including from goroutines a constructor starts for itself: the resolution path is immutable, so branches that run in parallel share nothing. A constructor may also keep the Scope it was handed and resolve through it later, once its own service is built; the finished part of that path is no longer a dependency, so such a resolution is not a cycle. A service is built once however many resolutions race for it, and a resolution of a running scope returns only a service whose start step has finished. A cycle is reported as ErrCycle even when the two halves are being built concurrently.
Three re-entrancy limits apply. A goroutine started by a constructor must use Scope.Resolve rather than Scope.Get, because Get reports failure by panicking and that panic cannot unwind to the enclosing call from another goroutine. An Binding.OnStart hook must not resolve a service that depends on the one being started: the hook already holds the value, and waiting for itself cannot make progress. And no hook may call Scope.Stop on its own scope or an ancestor, because Stop waits for the very step the hook is running; call Scope.Shutdown, which never blocks.
Example ¶
package main
import (
"context"
"fmt"
"github.com/floatdrop/di"
)
type Server struct{ repo *Repo }
func main() {
app := di.New()
app.Value(Config{DSN: "pg://primary"})
app.Provide(func(s *di.Scope) *DB { return &DB{dsn: s.Get[Config]().DSN} }).
OnStop(func(ctx context.Context, db *DB) error { fmt.Println("close", db.dsn); return nil })
app.Provide(func(s *di.Scope) *Repo { return &Repo{db: s.Get[*DB]()} })
app.Provide(func(s *di.Scope) Reader { return s.Get[*Repo]() })
app.Provide(func(s *di.Scope) *Server { return &Server{repo: s.Get[*Repo]()} }).Eager().
OnStart(func(context.Context, *Server) error { fmt.Println("listening"); return nil }).
OnStop(func(context.Context, *Server) error { fmt.Println("stopped"); return nil })
if err := app.Start(context.Background()); err != nil {
panic(err)
}
fmt.Println("read:", app.Get[Reader]().Read())
_ = app.Stop(context.Background())
}
Output: listening read: pg://primary stopped close pg://primary
Index ¶
- Variables
- func WithScope(ctx context.Context, s *Scope) context.Context
- type Binding
- func (b Binding[T]) Eager() Binding[T]
- func (b Binding[T]) Group() Binding[T]
- func (b Binding[T]) OnDrain(f func(context.Context, T) error) Binding[T]
- func (b Binding[T]) OnStart(f func(context.Context, T) error) Binding[T]
- func (b Binding[T]) OnStop(f func(context.Context, T) error) Binding[T]
- func (b Binding[T]) Scoped() Binding[T]
- func (b Binding[T]) Worker(f func(context.Context, T) error) Binding[T]
- type Event
- type EventKind
- type RunOption
- type Scope
- func (s *Scope) All[T any]() []T
- func (s *Scope) Child(name string) *Scope
- func (s *Scope) Context() context.Context
- func (s *Scope) Get[T any]() T
- func (s *Scope) Maybe[T any]() (T, bool)
- func (s *Scope) Must[T any](v T, err error) T
- func (s *Scope) Observe(fn func(Event))
- func (s *Scope) Provide[T any](ctor func(*Scope) T) Binding[T]
- func (s *Scope) Resolve[T any]() (v T, err error)
- func (s *Scope) Run(ctx context.Context, opts ...RunOption) error
- func (s *Scope) Shutdown(cause error)
- func (s *Scope) Start(ctx context.Context) error
- func (s *Scope) Stop(ctx context.Context) error
- func (s *Scope) Value[T any](v T) Binding[T]
- type TB
Examples ¶
Constants ¶
This section is empty.
Variables ¶
Functions ¶
Types ¶
type Binding ¶
type Binding[T any] struct { // contains filtered or unexported fields }
Binding is the typed handle returned by Provide/Value. Its methods refine the registration; they must be called before the first resolution from this scope.
func (Binding[T]) Eager ¶
Eager builds the service during Start rather than on first use.
Eagerness belongs to the key, not the registration: it means the service exists by the time Start returns. Overriding an eager binding therefore keeps the key eager and builds the replacement; a replacement with a per-scope lifetime, which cannot be built once at Start, is rejected.
func (Binding[T]) Group ¶ added in v0.7.0
Group makes the binding a member of the multi-binding group for T instead of the binding for T: it neither shadows nor is shadowed by another registration of T, and the members are read back together with s.All[T](). A member keeps its own lifetime and hooks.
func (Binding[T]) OnDrain ¶ added in v0.4.0
OnDrain runs before anything is stopped: Stop drains the whole tree, from the innermost scope outwards and in reverse build order, while every scope still resolves normally. It is where a service stops accepting new work and waits for the work it already has, such as an HTTP server that must finish in-flight requests whose handlers still need their request scope. Anything those handlers build, including a request scope of their own, is drained before the phase ends. Use OnStop for the release that follows.
func (Binding[T]) OnStart ¶
Typed lifecycle hooks: no interface sniffing, no reflection.
OnStart runs once the service is built, and only a hook that returns normally starts it: one that panics fails the start step, like a panicking constructor, and the service is never served.
func (Binding[T]) Scoped ¶
Scoped makes the binding one-per-scope: each scope that resolves it gets its own instance, built in that scope (so it can see that scope's values) and stopped with it. Declare request-scoped services once in the root and resolve them through the request scope.
Example ¶
package main
import (
"context"
"fmt"
"github.com/floatdrop/di"
)
type Session struct{ ID string }
func main() {
app := di.New()
// Declared once in the root, built once per scope that resolves it.
app.Provide(func(s *di.Scope) *Session { return &Session{ID: s.Get[string]()} }).Scoped()
for _, id := range []string{"a1", "b2"} {
req := app.Child("request")
req.Value(id)
first, again := req.Get[*Session](), req.Get[*Session]()
fmt.Println(first.ID, first == again)
_ = req.Stop(context.Background())
}
}
Output: a1 true b2 true
func (Binding[T]) Worker ¶ added in v0.7.0
Worker registers a long-running function for T, such as a consumer loop. It is started in its own goroutine once the service starts and its context is cancelled when the service stops; Stop waits for it to return, bounded by its own context. A hook that outlasts that deadline is reported by Stop, and OnStop then waits for it rather than releasing the value underneath a worker still reading it.
Returning a non-nil error calls Shutdown with it, stopping the application, even if the scope was already stopping: a worker may fail, flush while the scope winds down, and only then report. The exception is context.Canceled from a hook that was already cancelled, which is a worker reporting the cancellation and nothing else. A hook that wants to stay quiet during shutdown should return nil.
Example ¶
package main
import (
"context"
"fmt"
"github.com/floatdrop/di"
)
type Queue struct{ jobs chan string }
func main() {
app := di.New()
done := make(chan string, 1)
app.Provide(func(*di.Scope) *Queue { return &Queue{jobs: make(chan string, 1)} }).Eager().
Worker(func(ctx context.Context, q *Queue) error {
for {
select {
case job := <-q.jobs:
done <- "processed " + job
case <-ctx.Done():
return nil // cancelled by Stop
}
}
})
ctx := context.Background()
if err := app.Start(ctx); err != nil {
panic(err)
}
app.Get[*Queue]().jobs <- "email"
fmt.Println(<-done)
fmt.Println("stop:", app.Stop(ctx)) // waits for the worker to return
}
Output: processed email stop: <nil>
type Event ¶
type Event struct {
Kind EventKind
Service string // the service, e.g. "*github.com/acme/app.DB"; empty for shutdown
Scope string // name of the scope that owns the instance
Site string // file:line of the registration; empty for shutdown
Duration time.Duration
Err error
}
Event describes one lifecycle step. Observers receive it after the step completes, with its duration and error if any.
type EventKind ¶
type EventKind string
EventKind classifies an Event.
const ( EventBuild EventKind = "build" // a constructor ran EventStart EventKind = "start" // an OnStart hook ran EventDrain EventKind = "drain" // an OnDrain hook ran EventStop EventKind = "stop" // a Worker hook was cancelled and/or an OnStop hook ran EventShutdown EventKind = "shutdown" // Shutdown was called )
type RunOption ¶
type RunOption func(*runConfig)
RunOption configures Run.
func StopTimeout ¶
StopTimeout bounds how long Stop may take once Run decides to exit. The default is 15 seconds.
type Scope ¶
type Scope struct {
// contains filtered or unexported fields
}
Scope is a container. A Scope value handed to a constructor is a view over the same state that carries the current resolution path.
func FromContext ¶
FromContext returns the scope attached with WithScope, if any.
func Test ¶ added in v0.2.0
Test returns a scope for a test: the wire functions register the graph under test, and the scope is stopped when the test ends, failing it if a stop hook errors. Override what you need faked after wiring and before resolving; the last registration wins.
s := di.Test(t, app.Wire)
s.Value(&DB{DSN: "sqlite://memory"})
repo := s.Get[*Repo]()
func (*Scope) All ¶
All resolves the multi-binding group for T across the scope chain. Members are singletons (or Scoped if so marked) with the same lifecycle as any other binding.
func (*Scope) Child ¶
Child creates a scope that resolves through s. Stopping s stops its children first.
A child made inside a constructor carries that constructor's resolution path, so a cycle through it is reported rather than deadlocking. The path goes inert when the constructor returns (see resolver.done), so a child kept for later, such as a request scope, resolves as an independent branch.
func (*Scope) Context ¶
Context returns the context passed to Start (or Run) on this scope or the nearest started ancestor, so constructors can dial with a deadline. Before Start it returns context.Background().
func (*Scope) Get ¶
Get resolves T. Inside a constructor, failure unwinds to the enclosing Resolve/Start call and becomes an error; at top level it panics. In a goroutine a constructor started, use Resolve instead: that panic has no enclosing call to unwind to and would take the process down.
func (*Scope) Must ¶
Must unwraps a (value, error) pair inside a constructor:
db := s.Must(sql.Open("postgres", dsn))
A non-nil error aborts the constructor and surfaces from the enclosing Resolve, Start or Run. Outside a constructor it panics with the error.
func (*Scope) Observe ¶
Observe registers fn to receive lifecycle events from this scope and every scope under it. Use it for logging and metrics.
Example ¶
package main
import (
"context"
"errors"
"fmt"
"github.com/floatdrop/di"
)
type Cache struct{}
func main() {
app := di.New()
app.Observe(func(ev di.Event) {
fmt.Println(ev.Kind, ev.Service, ev.Err)
})
app.Provide(func(*di.Scope) *Cache { return &Cache{} }).
OnStop(func(context.Context, *Cache) error { return errors.New("flush failed") })
app.Get[*Cache]()
_ = app.Stop(context.Background())
}
Output: build *github.com/floatdrop/di_test.Cache <nil> stop *github.com/floatdrop/di_test.Cache di: stopping *github.com/floatdrop/di_test.Cache: flush failed
func (*Scope) Provide ¶
Provide registers a lazily built singleton. T is inferred from the constructor's return type; dependencies are pulled with s.Get[...]().
func (*Scope) Run ¶
Run starts the scope and blocks until ctx is cancelled, a termination signal arrives, or Shutdown is called. It then stops the scope with a bounded context; a second signal during the stop cancels that context so a hung hook cannot keep the process alive. Run returns the Start error, the error passed to Shutdown, and any Stop errors, joined. A worker that died on its own is reported once, whether it reached Run as the cause or as a Stop error.
func (*Scope) Shutdown ¶
Shutdown asks a running Run to stop and records the cause it should return. It never blocks, may be called from any goroutine, and the first call wins. It propagates to ancestor scopes, so a service in a child scope can stop the application.
func (*Scope) Start ¶
Start builds every Eager binding in registration order, then runs the start step of everything built so far, in build order. If a constructor or a start step fails, the scope is stopped, which rolls back exactly the services that did start, child scopes included. A service that was built but never started is not stopped, so acquire resources in OnStart rather than in the constructor when the binding declares one.
After Start returns, a service built later runs its start step as part of being built, so lazily resolved services start too. Start may be called once, and builds only this scope's own Eager bindings: a child scope's are built by that child's Start.
func (*Scope) Stop ¶
Stop winds the scope down in three phases. First it drains: OnDrain hooks run from the innermost scope outwards, in reverse build order, while every scope still resolves, so work already in flight can finish and still reach its dependencies. A service or child scope that phase brings into being is drained too, before anything is marked stopped. Then the scope is marked stopped and child scopes are stopped. Then OnStop hooks run in reverse build order (dependents first).
A service is stopped only if it started, or if it declares no OnStart, in which case OnStop is a plain destructor. Every failure is reported.
Stop is synchronous. It waits out whatever another goroutine is still running for a service it is tearing down -- a start step in flight, a drain hook another Stop began, a Worker hook being cancelled -- so when it returns, the teardown has happened and its failures are in the error. A teardown outlives the call in one case, when ctx expires first: the missed deadline is reported here, and the release is finished once the outstanding step returns, on a goroutine of its own, reaching observers rather than this caller. A constructor that finishes after the scope has stopped is likewise undone by whichever goroutine was resolving it.
Afterwards the scope and its descendants refuse to resolve anything, with ErrStopped; that includes a resolution that was already waiting when the scope stopped, so a closed service is never handed out. Stopping a child scope also detaches it from its parent, so per-request scopes are released once stopped.
Stop is idempotent, and concurrent calls are safe: only the first tears the scope down, and the others wait for it and report its result, bounded by their own context. Two Stop calls that meet at one scope, as a child and its parent do, wait for each other phase by phase, so neither starts releasing what the other's hooks are still using.
Because Stop waits, a hook must not call Stop on its own scope or an ancestor: it would be waiting for the step it is itself running. Stopping a sibling, or a scope below the hook's own, is allowed. A hook that passes on the context it was given gets an error saying so; one that passes a context of its own is not recognised, and waits until that context expires. Call Shutdown, which never blocks.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package dihttp connects a di.Scope to net/http.
|
Package dihttp connects a di.Scope to net/http. |
|
examples
|
|
|
app
command
A complete service: request scopes through middleware, a background worker with a Worker hook, a health endpoint, and graceful shutdown.
|
A complete service: request scopes through middleware, a background worker with a Worker hook, a health endpoint, and graceful shutdown. |
|
quickstart
command
Quick start: register a few services, start and stop the application.
|
Quick start: register a few services, start and stop the application. |
|
scopes
command
Scopes: a child scope sees everything in its parent and can shadow it.
|
Scopes: a child scope sees everything in its parent and can shadow it. |
|
server
command
Graceful shutdown of an HTTP server.
|
Graceful shutdown of an HTTP server. |
|
testing
Package app shows how to substitute dependencies in tests: wire the production graph into a fresh scope, then override before resolving.
|
Package app shows how to substitute dependencies in tests: wire the production graph into a fresh scope, then override before resolving. |