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.Transient builds an untracked new instance on every resolution. Scope.Add and Scope.All handle groups, Scope.Bind aliases an interface to an implementation, and Scope.Maybe resolves optional dependencies.
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, Scope.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.Run runs a long-lived function that is cancelled on stop, and Binding.Health feeds Scope.HealthCheck. 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.
Two 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.
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.Bind[Reader, *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 SlogObserver(l *slog.Logger) func(Event)
- func WithScope(ctx context.Context, s *Scope) context.Context
- type Binding
- func (b Binding[T]) Eager() Binding[T]
- func (b Binding[T]) Health(f func(context.Context, T) error) Binding[T]
- func (b Binding[T]) Named(name string) 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]) Run(f func(context.Context, T) error) Binding[T]
- func (b Binding[T]) Scoped() Binding[T]
- func (b Binding[T]) Transient() Binding[T]
- type Event
- type EventKind
- type Key
- type RunOption
- type Scope
- func (s *Scope) Add[T any](ctor func(*Scope) T) Binding[T]
- func (s *Scope) All[T any]() []T
- func (s *Scope) Bind[I, T any]() Binding[I]
- func (s *Scope) Child(name string) *Scope
- func (s *Scope) Context() context.Context
- func (s *Scope) Get[T any]() T
- func (s *Scope) HealthCheck(ctx context.Context) error
- func (s *Scope) Lookup[T any](k Key[T]) T
- func (s *Scope) Maybe[T any]() (T, bool)
- func (s *Scope) Middleware(next http.Handler) http.Handler
- 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 ¶
func SlogObserver ¶
SlogObserver returns an observer that logs every event to l: failures at Error level, everything else at Debug.
Types ¶
type Binding ¶
type Binding[T any] struct { // contains filtered or unexported fields }
Binding is the typed handle returned by Provide/Value/Bind/Add. 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.
Lifetime and lifecycle hooks belong to a registration, because they are typed on that particular value, but eagerness belongs to the key: it means the service exists by the time Start returns. So overriding an eager binding keeps the key eager and builds the replacement, while a replacement with a per-scope lifetime, which cannot be built once at Start, is rejected.
func (Binding[T]) Health ¶
Health registers a health check for T, run by HealthCheck once the service has been built.
Example ¶
package main
import (
"context"
"errors"
"fmt"
"github.com/floatdrop/di"
)
type Cache struct{ ok bool }
func main() {
app := di.New()
app.Provide(func(*di.Scope) *Cache { return &Cache{} }).
Health(func(ctx context.Context, c *Cache) error {
if !c.ok {
return errors.New("not connected")
}
return nil
})
fmt.Println("before build:", app.HealthCheck(context.Background()))
app.Get[*Cache]()
err := app.HealthCheck(context.Background())
fmt.Println("unhealthy:", errors.Is(err, di.ErrUnhealthy))
fmt.Println(err)
}
Output: before build: <nil> unhealthy: true di: *github.com/floatdrop/di_test.Cache unhealthy: not connected
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]) Run ¶
Run registers a long-running function for T, such as a worker 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. That holds whenever the scope was stopping, because when the failure surfaced says nothing about what caused it: a worker may fail, flush what it has while the scope winds down, and only then report. The one error that does not count is context.Canceled from a hook we had already cancelled, which is a worker reporting the cancellation and nothing else. A hook that would rather 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().
Run(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>
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
type Event ¶
type Event struct {
Kind EventKind
Service string // the service, e.g. "*github.com/acme/app.DB" or "...DB#replica"; 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 Run hook was cancelled and/or an OnStop hook ran EventHealth EventKind = "health" // a Health hook ran EventShutdown EventKind = "shutdown" // Shutdown was called )
type Key ¶
type Key[T any] struct { // contains filtered or unexported fields }
Key identifies a service: its Go type plus an optional name. Keys compare by reflect.Type identity, so same-named types in different packages never collide (unlike fmt.Sprintf("%T")-derived names).
type RunOption ¶
type RunOption func(*runConfig)
RunOption configures Run.
func Signals ¶
Signals replaces the signals that make Run exit. The default is os.Interrupt and SIGTERM.
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/Transient if so marked) with the same lifecycle as any other binding.
func (*Scope) Bind ¶
Bind registers an alias: requests for I are served by T's binding, with T's lifetime, instance and hooks. Both parameters are explicit: s.Bind[Reader, *Repo](). Lifetimes and lifecycle hooks belong on the target binding, not on the alias.
func (*Scope) Child ¶
Child creates a scope that resolves through s. Stopping s stops its children first.
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) HealthCheck ¶
HealthCheck runs every Health hook of the services built in this scope and its descendants, concurrently, and returns the failures joined. Each failure wraps ErrUnhealthy and names the service.
func (*Scope) Middleware ¶
Middleware gives every request its own child scope: the *http.Request is registered in it, the scope is attached to the request context, and it is stopped (and detached) when the handler returns.
Example ¶
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"github.com/floatdrop/di"
)
type Caller struct{ Name string }
func main() {
app := di.New()
app.Provide(func(s *di.Scope) *Caller {
return &Caller{Name: s.Get[*http.Request]().Header.Get("X-Caller")}
}).Scoped()
h := app.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
req, _ := di.FromContext(r.Context())
fmt.Fprintln(w, "hello", req.Get[*Caller]().Name)
}))
rec := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/", nil)
r.Header.Set("X-Caller", "ada")
h.ServeHTTP(rec, r)
fmt.Print(rec.Body.String())
}
Output: hello ada
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; see SlogObserver.
Example ¶
package main
import (
"context"
"errors"
"fmt"
"github.com/floatdrop/di"
)
type Cache struct{ ok bool }
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, recording why: it wakes any waiting Run and records the cause it should return. It never blocks, may be called from any goroutine, and the first call wins. The request 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 actually started, or if it declares no OnStart, in which case OnStop is a plain destructor. Every failure is reported. Two teardowns can outlive the call, both reported to observers rather than returned here: a service whose start step was in flight is torn down by the goroutine running that step, and a service whose Run hook outlasts ctx is released once that hook returns, rather than while it is still using the value.
Afterwards the scope and its descendants refuse to resolve anything, with ErrStopped, so a closed service can never be handed out, and so can a resolution that was already waiting when the scope stopped. 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. For that reason a hook must not call Stop on its own scope or an ancestor, which would be a wait on itself; call Shutdown, which never blocks.
Directories
¶
| Path | Synopsis |
|---|---|
|
examples
|
|
|
app
command
A complete service: request scopes through middleware, a background worker with a Run hook, a health endpoint, and graceful shutdown.
|
A complete service: request scopes through middleware, a background worker with a Run 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. |