Documentation
¶
Overview ¶
Package di is a dependency-injection container for Go 1.27+ built on generic methods: services are registered with s.Provide(...) and resolved with s.Get[T]().
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]
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 (*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. |