floatdrop/di
Dependency injection container built on generic methods, with child scopes, lifecycle hooks and graph validation before anything is built
About floatdrop/di
This large README is shown as a plain-text preview. Read the full README at the source
<p align="center">
<img src="docs/assets/logo.svg" width="280" alt="di — a Go gopher joining two coral cable connectors" />
</p>
# di
[](https://github.com/yandex/di/actions/workflows/ci.yml)
[](https://pkg.go.dev/golang.yandex/di)
[](https://yandex.github.io/di/coverage.html)
[](LICENSE)
[](https://floatdrop.github.io/awesome-go/#yandex--di)
A dependency-injection container for Go 1.27+. Constructors are plain
functions, keys are Go types, and the container builds, starts and stops
services in dependency order.
```go
app := di.New()
app.Value(Config{DSN: "postgres://localhost/app"})
app.Wire[*DB](NewDB) // func NewDB(Config) (*DB, error)
app.Wire[*Repo](NewRepo) // func NewRepo(*DB) *Repo
repo := app.Get[*Repo]() // builds Config, then DB, then Repo, each once
```
There is no code generation and no dependency outside the standard library.
`Wire` reads a constructor's signature once, with reflection, which is how
`Validate` can check the whole graph before a single constructor runs.
`Provide` takes a closure for the rare constructor that needs the scope
itself.
[**The guide**](https://yandex.github.io/di/) walks through one application
file by file. [**How it works**](docs/DESIGN.md) explains what happens between
`Get` and a value, with diagrams.
## Installation
```sh
go get golang.yandex/di
```
Requires Go 1.27 or newer. Editor support for generic methods needs gopls
v0.23 or newer.
## Quick start
[embedmd]:# (examples/quickstart/main.go go)
```go
// Quick start: register a few services, start and stop the application.
package main
import (
"context"
"fmt"
"log"
"golang.yandex/di"
)
type Config struct{ DSN string }
type DB struct{ dsn string }
type Repo struct{ db *DB }
type Server struct{ repo *Repo }
// Plain constructors: their parameters are their dependencies.
func NewDB(cfg Config) *DB { return &DB{dsn: cfg.DSN} }
func NewRepo(db *DB) *Repo { return &Repo{db: db} }
func NewServer(repo *Repo) *Server { return &Server{repo: repo} }
func main() {
app := di.New()
app.Value(Config{DSN: "postgres://localhost/app"})
app.Wire[*DB](NewDB).
OnStop(func(ctx context.Context, db *DB) error { fmt.Println("db closed"); return nil })
app.Wire[*Repo](NewRepo)
app.Wire[*Server](NewServer).
Eager().
OnStart(func(ctx context.Context, srv *Server) error { fmt.Println("listening"); return nil }).
OnStop(func(ctx context.Context, srv *Server) error { fmt.Println("server stopped"); return nil })
ctx := context.Background()
if err := app.Start(ctx); err != nil {
log.Fatal(err)
}
defer func() {
if err := app.Stop(ctx); err != nil {
log.Println(err)
}
}()
fmt.Println("serving", app.Get[*Server]().repo.db.dsn)
}
```
## Guide
The sections follow the order an application comes together. Every
embedded example is a compiled program under [`examples/`](examples/), built
in CI.
### Registering and resolving
Each registration returns a `Binding[T]`. Its methods must be called before
the scope is first resolved.
| Call | Registers |
|---|---|
| `s.Provide(func(*di.Scope) T)` | A lazily built singleton. `T` is inferred. |
| `s.Wire[T](NewT)` | A lazily built singleton from a plain constructor. Its parameters are its dependencies; see [Validate](#validate). |
| `s.Wrap[T](fn)` | A wrapper over what serves `T`: `fn` takes that value first, then its dependencies; see [Wrapping a service](#wrapping-a-service). |
| `s.Value(v)` | An instance you already have. |
| `s.Use(mods...)` | What the modules register, attributed to them by name. |
| Method | Effect |
|---|---|
| `.Scoped()` | One instance per resolving scope, built and stopped there. |
| `.Group()` | A member of the group for `T`, read back with `s.All[T]()`. |
| `.Eager()` | Build during `Start`, in registration order. |
| `.Needs(...)` | How to fill a `Wire` parameter that is not a plain dependency: `di.AllOf[T]()` for a `[]T` holding the group, `di.Optional[T]()` for one that may go unprovided. |
| `.Override()` | Replace an earlier registration of `T` in this scope; a second one without it is rejected. |
| `.OnStart(f)`, `.OnStop(f)` | Lifecycle hooks, `f` is `func(context.Context, T) error`. |
| `.OnDrain(f)` | Runs before anything is stopped, while the scope still resolves. |
| `.Go(f)` | A worker: a long-running function in a goroutine of its own, cancelled on stop. |
To get a service back:
| Call | Returns |
|---|---|
| `s.Get[T]()` | `T`. Inside a constructor a failure unwinds to the caller; at top level it panics with the error. |
| `s.Resolve[T]()` | `(T, error)`. Never panics on a wiring problem. |
| `s.Maybe[T]()` | `(T, bool)`; see [Optional dependencies](#optional-dependencies). |
| `s.All[T]()` | Every member of the group for `T`, across the scope chain. |
| `s.Must(v, err)` | `v`, or aborts the constructor with `err`. |
| `s.Context()` | The context passed to `Start`, so constructors can dial with a deadline. |
Errors wrap `di.ErrNotProvided`, `di.ErrCycle` or `di.ErrStopped`:
```
di: building *app.Repo (provided at app/wire.go:31): *app.DB: not provided (needed by [*app.Repo])
di: building *app.A (provided at ...): di: building *app.B (provided at ...): di: dependency cycle: [*app.A *app.B] -> *app.A
```
`Provide` takes a closure that pulls its dependencies with `s.Get`. `Wire`
takes a plain constructor, `func(A, B) T` or `func(A, B) (T, error)`; its
parameters are its dependencies, so `Validate` can check them before
anything runs, and a wrong shape is rejected at registration. A slice
parameter is a key rather than a group unless `Needs(di.AllOf[T]())` says
otherwise. A constructor that needs the scope itself stays a closure.
An interface is served by a constructor that returns the implementation:
```go
app.Provide(func(s *di.Scope) Reader { return s.Get[*Repo]() })
app.Wire[Reader](NewRepo) // the same, when NewRepo returns *Repo
```
Both keys share one instance. Mark it `Scoped()` too when the target is.
`Wire` accepts any result assignable to the key.
Three rules, checked when the scope is next resolved:
- A second registration of a key in one scope must say `Override()`, or it
is rejected naming both sites; so is an `Override()` with nothing to
override. It inherits the key's eagerness. A child shadows its parent's
key without the marker.
- A key that has served a value cannot be replaced, in its own scope or in
any scope that resolved through it.
- `Eager` on a `Scoped` binding, and `Scoped` on a `Value`, are rejected.
#### Optional dependencies
A dependency nothing provides is an error unless the registration says
otherwise. `Needs` says so, and the constructor stays a function anyone can
call:
```go
// func NewReport(s *Store, t *Tracer) *Report
app.Wire[*Report](NewReport).Needs(di.Optional[*Tracer]())
```
`*Tracer` is passed if anything provides it and nil if nothing does. The
marker is on the registration rather than on the parameter, so it is fx's
`optional:"true"` without a struct tag — and because it is declared,
`Validate` checks the rest of that constructor, `Explain` draws the
parameter as `not provided, optional`, and `Modules` lists it.
Two alternatives are often better than optionality at all:
```go
app.Value[*Cache](nil) // a nil default: provided, checked, overridden where there is one
app.Wire[Metrics](NewNopMetrics) // a null object: nothing downstream has a branch
```
`Scope.Maybe` asks the same question from inside a closure, for a key
nothing registers at all. It is the escape hatch: a closure declares
nothing, so `Validate` lists it as unchecked.
Whether a key is provided is a question about the chain as it stands — a
child scope may answer it differently — so registering it later is never
rejected, and asking then providing a default works:
```go
if _, ok := app.Maybe[*Cache](); !ok {
app.Value(newCache())
}
```
A service built before the key was registered keeps the answer it got,
though, and `Explain` names it, which is where a dependency wired too late
shows up:
```
*app.Tracer: value in root, not built (provided at tracing.go:8)
missed by: *app.Router in root
```
### Lifecycle
`Start` builds every `Eager` binding, then runs `OnStart` hooks in build
order. If one fails, `Start` stops what had started, child scopes included,
and returns both errors. A service built after `Start` runs its `OnStart` as
it is built.
`Stop` drains, stops the child scopes, then runs `OnStop` hooks in reverse
build order, and joins every failure into its error. `OnStop` runs when
`OnStart` succeeded or when there is none. After `Stop`, the scope and
everything under it resolves with `di.ErrStopped`.
`Stop` is safe to call twice or concurrently: later calls wait for the first
and report its result. So a hook must not call `Stop` on its own scope or an
ancestor; it calls `Shutdown`, which never blocks.
`OnStart` returns when the service is ready. A server binds its listener in
the hook, so a busy port fails `Start`, and serves in a goroutine.
A binding carries one hook of each kind. A second `OnStart`, `OnDrain`,
`OnStop` or `Go` is rejected, naming the registration and the second call,
rather than replacing the first: put the whole step in one function, where
its order is yours.
#### Draining
`OnDrain` runs before anything is stopped, innermost scope first, while
every scope still resolves. It is where a service stops taking work and
finishes what it has.
```go
app.Wire[*http.Server](newServer).Eager().
OnDrain(func(ctx context.Context, srv *http.Server) error { return srv.Shutdown(ctx) }).
OnStop(func(ctx context.Context, srv *http.Server) error { return srv.Close() })
```
An HTTP server's handlers hold request scopes, and shutting it down from
`OnStop` would race their teardown. Draining first keeps them alive until
the handlers return.
#### Workers
`Go` registers a worker: a function that loops until told to stop, in a
goroutine of its own. It is errgroup's `Go`, applied to a service.
```go
app.Wire[*Mailer](newMailer).Eager().Go(func(ctx context.Context, m *Mailer) error {
return m.Loop(ctx) // returns when ctx is cancelled
})
```
The context is cancelled when the service stops, and `Stop` waits for the
function within its deadline. A worker that returns an error calls
`Shutdown`; `context.Canceled` after cancellation means nothing.
A worker starts as soon as its own service has, while services after it may
still be starting. `Ready` is closed once the whole `Start` succeeds and
never if it fails, so a server binds in `OnStart` and serves from its worker
only after `Ready`, selecting on its context too: a failed start then rolls
back without a request served. A scope's `Ready` follows the nearest `Start`
at or above it, as `Context` does. `dihttp.Serve` does all of this for an
`*http.Server`.
#### Run and Shutdown
`Run` is the helper for `main`: start, block until the context is cancelled,
`SIGINT` or `SIGTERM` arrives, or `Shutdown` is called, then stop with a
bounded context. A second signal cancels that context.
Both phases are bounded, 15 seconds each by default. `StartTimeout` expires
the context the `OnStart` hooks receive and ends the start between steps, so
a hook waiting on something that never arrives fails and rolls back instead
of hanging the process; `StartTimeout(0)` takes the bound off.
It bounds that phase and no more. A constructor reads `Scope.Context`, which
is the context `Run` was called with, and a worker runs for as long as its
service. A service resolved from *inside* a start hook is started there and
then, on the scope's context, so a hook that waits on such a service is not
bounded either — the deadline is checked between the steps `Start` drives.
<details>
<summary><code>examples/server/main.go</code>, an HTTP server with OnStart, Go, OnDrain and OnStop, run with a stop timeout</summary>
[embedmd]:# (examples/server/main.go go)
```go
// Graceful shutdown of an HTTP server.
//
// Run starts the scope, waits for SIGINT/SIGTERM or a Shutdown call, then
// stops everything in reverse order with a bounded context. The server's
// OnDrain calls http.Server.Shutdown, which stops accepting connections and
// waits for in-flight requests until the stop context expires. Draining runs
// before anything is torn down, so those requests still have their scopes.
package main
import (
"context"
"errors"
"fmt"
"log"
"net"
"net/http"
"time"
"golang.yandex/di"
)
type DB struct{ dsn string }
func main() {
app := di.New()
app.Wire[*DB](func() *DB { return &DB{dsn: "postgres://localhost/app"} }).
OnStop(func(ctx context.Context, db *DB) error { log.Println("db closed"); return nil })
app.Wire[http.Handler](func(db *DB) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(2 * time.Second) // simulate slow work that must not be cut short
fmt.Fprintln(w, "served by", db.dsn)
})
})
var ln net.Listener
app.Wire[*http.Server](func(h http.Handler) *http.Server { return &http.Server{Addr: ":8080", Handler: h} }).
Eager().
OnStart(func(ctx context.Context, srv *http.Server) (err error) {
// Bind synchronously so a busy port fails Start.
ln, err = net.Listen("tcp", srv.Addr)
if err == nil {
log.Println("listening on", ln.Addr())
}
return err
}).
// Serve once the whole application has started; a failed start rolls
// back without a request served. An error here stops the application.
Go(func(ctx context.Context, srv *http.Server) error {
select {
case <-app.Ready():
case <-ctx.Done():
_ = ln.Close() // the start failed and rolled back
return nil
}
if err := srv.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
}).
// OnDrain runs before anything is stopped, so handlers that are
// still running keep their scopes and dependencies.
OnDrain(func(ctx context.Context, srv *http.Server) error {
log.Println("draining")
return srv.Shutdown(ctx) // waits for in-flight requests, bounded by StopTimeout
}).
OnStop(func(ctx context.Context, srv *http.Server) error { return srv.Close() })
// Blocks until Ctrl-C, SIGTERM, or app.Shutdown. A second signal cancels
// the stop context so a hung hook cannot keep the process alive.
if err := app.Run(context.Background(), di.StopTimeout(10*time.Second)); err != nil {
log.Fatal(err)
}
}
```
</details>
### Scopes
A child scope resolves through its parent, shares its singletons, and owns
what it builds itself.
[embedmd]:# (examples/scopes/main.go go)
```go
// Scopes: a child scope sees everything in its parent and can shadow it.
package main
import (
"fmt"
"golang.yandex/di"
)
type DB struct{ dsn string }
type User struct{ Name string }
type Handler struct {
db *DB
user *User
}
func NewHandler(db *DB, user *User) *Handler { return &Handler{db: db, user: user} }
func main() {
app := di.New()
app.Wire[*DB](func() *DB { return &DB{dsn: "postgres://localhost/app"} })
// One child per request: request-scoped values live here, shared
// singletons such as *DB are reused from app.
req := app.Child("request")
req.Value(&User{Name: "ada"})
req.Wire[*Handler](NewHandler)
h := req.Get[*Handler]()
fmt.Println(h.user.Name, "->", h.db.dsn)
fmt.Println("same db:", h.db == app.Get[*DB]())
}
```
A singleton is built in the scope that registered it. A service that has
to see a child's values is `Scoped()`: one instance per resolving scope,
built there.
#### Request scopes
`dihttp.Middleware` gives each request a child scope holding the
`*http.Request`, puts it in the request context, and stops it when the
handler returns. It has the `func(http.Handler) http.Handler` shape.
`dihttp.Module` registers one:
```go
import "golang.yandex/di/dihttp"
app.Use(dihttp.Module, api.Module)
func NewServer(cfg Config, mw dihttp.Middleware) *http.Server {
mux := http.NewServeMux()
mux.Handle("GET /users/{id}", dihttp.Handle((*Users).Show))
mux.Handle("GET /healthz", dihttp.Handle((*Health).Check))
return &http.Server{Addr: cfg.Addr, Handler: mw(mux)}
}
```
`dihttp.Serve(app.Wire[*http.Server](NewServer))` gives that server its
lifecycle: it is eager, binds in `OnStart` so a busy port fails the start,
serves once the whole start has succeeded, drains in `OnDrain` and closes in
`OnStop`. A port of 0 in `Addr` is replaced with the one chosen. A second
server in the same scope, an admin port say, is registered under a type of
its own, `type AdminServer *http.Server`, and served by the same call; its
constructor still returns `*http.Server`. `examples/server` below writes
the same hooks by hand.
`dihttp.Handle` resolves a handler type from the request's scope and calls
the method; mark the type `Scoped()` when it needs the request.
`dihttp.HandleFunc` is the same as an `http.HandlerFunc`, for a router such as
chi whose `Get` and `Post` take one. Services
that depend on the request are declared once, in the root, as `Scoped()`:
```go
app.Wire[*User](func(r *http.Request) *User { return &User{Name: r.Header.Get("X-User")} }).Scoped()
```
For a server the container does not build, `dihttp.NewMiddleware(app)`
makes the middleware outside it and `di.FromContext` reaches the scope by
hand:
```go
mux.HandleFunc("GET /hello", func(w http.ResponseWriter, r *http.Request) {
req, _ := di.FromContext(r.Context()) // the request scope the middleware opened
fmt.Fprintln(w, "hello", req.Get[*User]().Name)
})
srv := &http.Server{Handler: dihttp.NewMiddleware(app)(mux)}
```
For gRPC, [`digrpc`](digrpc) is the same adapter as a separate module, so
the library itself does not depend on grpc: `go get golang.yandex/di/digrpc`.
Its `Interceptor` opens a scope per call holding a `*digrpc.Call`, the method
and the incoming context, and `Module` provides it. `digrpc.Register[*Users](srv, &pb.Users_ServiceDesc)`
serves a generated service with an implementation resolved from that scope,
so the implementation is a service like a handler type under `dihttp.Handle`:
`Scoped()` when it takes the call, built after the interceptors have run, and
a constructor's status error is the call's. `digrpc.Serve(b, listen)` runs
the server as `dihttp.Serve` does, on the listener `listen` returns, with
`GracefulStop` as the drain hook.
<details>
<summary><code>examples/grpc/main.go</code>, a gRPC server with a service built per call from the call's metadata</summary>
[embedmd]:# (examples/grpc/main.go go)
```go
// Graceful shutdown of a gRPC server, with a service built per call.
//
// The digrpc interceptor opens a scope for every call and registers the
// *digrpc.Call in it, and digrpc.Register serves the health service with an
// implementation resolved from that scope, so a Scoped Health is built per
// call with a Caller read from the call's metadata. digrpc.Serve gives the
// server its lifecycle: it binds in OnStart, serves once the whole start has
// succeeded, and drains with GracefulStop, which stops accepting calls and
// waits for in-flight ones. Run starts the scope, waits for SIGINT/SIGTERM or
// a Shutdown call, then stops everything in reverse order with a bounded
// context. Draining runs before anything is torn down, so those calls still
// have their scopes.
package main
import (
"cmp"
"context"
"log"
"net"
"strings"
"time"
"golang.yandex/di"
"golang.yandex/di/digrpc"
"google.golang.org/grpc"
"google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/metadata"
)
type DB struct{ dsn string }
// Caller is who is making the call, read from its metadata. It depends onFrequently Asked Questions
What is floatdrop/di?
floatdrop/di is a Miscellaneous library for the Go programming language. Dependency injection container built on generic methods, with child scopes, lifecycle hooks and graph validation before anything is built
How do I install floatdrop/di?
Install floatdrop/di with the Go module system using `go get floatdrop/di`. Check the repository for the current installation instructions.
What category does floatdrop/di belong to?
floatdrop/di is listed under Miscellaneous, specifically Dependency Injection.