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 a 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 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 stops child scopes first, 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.
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]) 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(err error)
- func (s *Scope) Start(ctx context.Context) (err 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]) 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]) 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. Returning a non-nil error before that calls Shutdown with it, stopping the application.
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 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. Both parameters are explicit: s.Bind[Reader, *Repo]().
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.
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.
func (*Scope) Shutdown ¶
Shutdown asks a running Run to stop, optionally recording why. 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, then runs OnStart hooks in build order. If a hook fails, the hooks that already ran are rolled back with OnStop in reverse order and the scope is left with nothing to stop. After Start returns, a binding built later runs its OnStart as part of being built, so lazily resolved services start too. Start may be called once.
func (*Scope) Stop ¶
Stop stops child scopes first, then runs OnStop hooks in reverse build order (dependents first). Every failure is reported; Stop is idempotent. Afterwards the scope and its descendants refuse to resolve anything, with ErrStopped, so a closed service can never be handed out. Stopping a child scope also detaches it from its parent, so per-request scopes are released once stopped.
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. |