di

package module
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 4, 2026 License: MIT Imports: 15 Imported by: 0

README

di

CI Go Reference

A dependency-injection container for Go 1.27+, built on generic methods. Register services with s.Provide(...), resolve them with s.Get[T](). No reflection over your constructors, no code generation, no dependencies.

app := di.New()
app.Provide(func(s *di.Scope) *DB { return s.Must(sql.Open("postgres", 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. No naming scheme, no string collisions between packages.
  • Constructors return T, not (T, error). A missing dependency or a failed constructor unwinds to the enclosing Resolve or Start as an error that names the full dependency path and the registration site.
  • Typed lifecycle. OnStart, OnStop, Run and Health hooks are typed on the service. Nothing is discovered by sniffing interfaces.
  • Deterministic shutdown. Reverse build order, child scopes first, every error reported.
  • Scopes for requests and tests. Child scopes shadow their parent; the last registration of a key wins until that key has served a value.

Installation

go get github.com/floatdrop/di

Requires Go 1.27 or newer. Editor support for generic methods needs gopls v0.23 or newer.

Quick start

Every example in this document is a compiled program under examples/ and is run in CI.

// Quick start: register a few services, start and stop the application.
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/floatdrop/di"
)

type Config struct{ DSN string }
type DB struct{ dsn string }
type Repo struct{ db *DB }
type Server struct{ repo *Repo }

func main() {
	app := di.New()

	app.Value(Config{DSN: "postgres://localhost/app"})

	app.Provide(func(s *di.Scope) *DB { return &DB{dsn: s.Get[Config]().DSN} }).
		OnStop(func(ctx context.Context, db *DB) error { fmt.Println("db closed"); return nil })

	app.Provide(func(s *di.Scope) *Repo { return &Repo{db: s.Get[*DB]()} })

	app.Provide(func(s *di.Scope) *Server { return &Server{repo: s.Get[*Repo]()} }).
		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

Registration

Each registration returns a typed Binding[T]. Its methods refine the registration and 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.Value(v) An instance you already have.
s.Bind[I, T]() An alias: I is served by T's binding, lifetime and hooks included.
s.Add(func(*di.Scope) T) A member of the group for T.
Method Effect
.Named("replica") Also register under a name.
.Scoped() One instance per resolving scope, built and stopped there.
.Transient() A new, untracked instance on every resolution.
.Eager() Build during Start, in registration order.
.OnStart(f), .OnStop(f) Lifecycle hooks, f is func(context.Context, T) error.
.Run(f) A long-running function, cancelled on stop.
.Health(f) A health check, run by HealthCheck.

Rules the container enforces:

  • The last registration of a key wins. That is how a child scope shadows its parent and how a test substitutes a fake.
  • Once a key has served a value it can no longer be replaced, in the scope that owns it or in any scope that resolved through it. Replacing it would leave one key with two live values, so it panics instead. A resolution that failed built nothing and leaves the key re-registerable.
  • Combinations that cannot be honoured are rejected when the scope is first resolved, whatever order the methods were called in: hooks or Eager on a transient, Eager on a scoped binding, a lifetime on a Value, and lifetimes or hooks on an alias.
Resolution
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.Lookup(di.Named[T]("replica")) A named binding, through a typed key.
s.Maybe[T]() (T, bool), for 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
Scopes

A child scope resolves through its parent, reuses the parent's singletons, and owns the lifecycle of what it builds itself.

// Scopes: a child scope sees everything in its parent and can shadow it.
package main

import (
	"fmt"

	"github.com/floatdrop/di"
)

type DB struct{ dsn string }
type User struct{ Name string }
type Handler struct {
	db   *DB
	user *User
}

func main() {
	app := di.New()
	app.Provide(func(*di.Scope) *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.Provide(func(s *di.Scope) *Handler {
		return &Handler{db: s.Get[*DB](), user: s.Get[*User]()}
	})

	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, so a child cannot rewire a parent's singleton. A service that must see child-scoped values is declared Scoped(): one instance per resolving scope, built there.

Lifecycle

Start builds every Eager binding, then runs OnStart hooks in build order. If a constructor or hook fails, Start stops the scope and returns both errors; that rolls back exactly the services that started, child scopes included. A service built after Start runs its OnStart as part of being built, so nothing is handed out unstarted.

Stop runs in three phases. It drains, then stops child scopes, then runs OnStop hooks in reverse build order, and returns every failure joined. A service is stopped only when its stop is owed: its OnStart succeeded, or it has no OnStart to pair with, in which case OnStop is a plain destructor. Afterwards the scope and everything under it refuses to resolve, with di.ErrStopped.

Stop is idempotent and safe to call concurrently: only the first call tears the scope down, and the others wait for it and report its result. A hook must therefore not call Stop on its own scope or an ancestor, which would be a wait on itself; call Shutdown, which never blocks.

Start hooks must not block. A server binds its listener synchronously, so a busy port fails Start, then serves in a goroutine.

Draining

OnDrain runs before anything is stopped, from the innermost scope outwards and in reverse build order, while every scope still resolves normally. It is where a service stops taking new work and waits for the work it already has.

app.Provide(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 is the case that needs it. Its handlers hold request scopes that are children of the application scope, so shutting the server down from OnStop would be racing the teardown of the scopes those handlers are still using: a request in flight would start failing with di.ErrStopped before the server had finished waiting for it. Draining first gives handlers their scopes and dependencies until they return.

Workers

Run is for anything that loops until told to stop: consumers, pollers, schedulers.

app.Provide(newMailer).Eager().Run(func(ctx context.Context, m *Mailer) error {
    return m.Loop(ctx) // returns when ctx is cancelled
})

The function runs in its own goroutine from the moment the service starts. Its context is cancelled when the service stops, and Stop waits for it within the stop deadline. Returning an error calls Shutdown, so a worker that dies takes the application down rather than leaving it half alive. That holds even if the worker only reports the failure once shutdown is under way, since when an error surfaces says nothing about what caused it; returning context.Canceled after cancellation is the one case that means nothing more than "I stopped".

Health checks
app.Provide(newDB).Health(func(ctx context.Context, db *DB) error { return db.Ping(ctx) })

mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
    if err := app.HealthCheck(r.Context()); err != nil {
        http.Error(w, err.Error(), http.StatusServiceUnavailable)
        return
    }
    fmt.Fprintln(w, "ok")
})

HealthCheck runs the Health hook of every service already built in the scope and its descendants, concurrently and bounded by the context. Each failure wraps di.ErrUnhealthy and names the service.

Request scopes

Middleware gives each request a child scope holding the *http.Request, attaches it to the request context, and stops it when the handler returns.

srv := &http.Server{Handler: app.Middleware(mux)}

mux.HandleFunc("GET /hello", func(w http.ResponseWriter, r *http.Request) {
    req, _ := di.FromContext(r.Context())
    user := req.Get[*User]()
})

Services that depend on the request are declared once, in the root, as Scoped(); they are built per request, cached for its duration, and stopped with it:

app.Provide(func(s *di.Scope) *User {
    return &User{Name: s.Get[*http.Request]().Header.Get("X-User")}
}).Scoped()

di.WithScope and di.FromContext are the primitives if you are not using net/http. A complete service with a worker, a health endpoint, request scopes and graceful shutdown is in examples/app.

Graceful shutdown

Run is the main-function helper. It starts the scope, blocks until the context is cancelled, SIGINT or SIGTERM arrives, or Shutdown is called, then stops everything with a bounded context. A second signal during the stop cancels that context, so a hung hook cannot keep the process alive.

// 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"

	"github.com/floatdrop/di"
)

type DB struct{ dsn string }

func main() {
	app := di.New()

	app.Provide(func(*di.Scope) *DB { return &DB{dsn: "postgres://localhost/app"} }).
		OnStop(func(ctx context.Context, db *DB) error { log.Println("db closed"); return nil })

	app.Provide(func(s *di.Scope) http.Handler {
		db := s.Get[*DB]()
		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)
		})
	})

	app.Provide(func(s *di.Scope) *http.Server {
		return &http.Server{Addr: ":8080", Handler: s.Get[http.Handler]()}
	}).
		Eager().
		OnStart(func(ctx context.Context, srv *http.Server) error {
			// Bind synchronously so a busy port fails Start; serve in the background.
			ln, err := net.Listen("tcp", srv.Addr)
			if err != nil {
				return err
			}
			log.Println("listening on", ln.Addr())
			go func() {
				if err := srv.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
					app.Shutdown(err) // the listener died: stop the whole application
				}
			}()
			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)
	}
}
Observability
app.Observe(di.SlogObserver(slog.Default()))

app.Observe(func(ev di.Event) {
    if ev.Kind == di.EventBuild {
        buildDuration.WithLabelValues(ev.Service).Observe(ev.Duration.Seconds())
    }
})

Observers receive an Event for every constructor and every OnStart, OnStop and Health hook in the scope and its descendants, plus one per Shutdown. Each event names the service, its scope, the registration site, the duration and the error, if any.

Testing your application

di.Test wires the production graph into a fresh scope and stops it when the test ends, failing the test if a stop hook errors. Override what you need before anything is resolved.

package app

import (
	"testing"

	"github.com/floatdrop/di"
)

func TestRepo(t *testing.T) {
	s := di.Test(t, Wire)                // production graph, stopped when the test ends
	s.Value(&DB{DSN: "sqlite://memory"}) // later registration wins: replaces the production *DB

	repo := s.Get[*Repo]() // built against the fake DB
	if repo.DB.DSN != "sqlite://memory" {
		t.Fatalf("got %q", repo.DB.DSN)
	}
}

Design notes

Why generic methods. Before Go 1.27 a typed container had to expose package-level functions such as do.Invoke[T](injector), and every variation became another function. With generic methods the whole API lives on one concrete type and reads left to right. The trade-off is that generic methods cannot appear on interfaces, so *di.Scope is concrete; substitute dependencies through scopes rather than by mocking the container.

Concurrency. Resolution is safe from many goroutines, including goroutines a constructor starts for itself. Each singleton is built at most once however many resolutions race for it, and the resolution path is an immutable linked list, so parallel branches share nothing. Once the scope is running, a resolution returns only a service whose OnStart has finished, waiting if another goroutine is starting it. A cycle is reported as di.ErrCycle even when the two halves are being built concurrently, which needs a wait-for graph rather than the per-branch path alone.

Two re-entrancy limits apply. In a goroutine a constructor started, use Resolve rather than Get: Get reports failure by panicking, and that panic has no enclosing call to unwind to from another goroutine. And an OnStart hook must not resolve a service that depends on the one being started, which would be a wait on itself.

Known limitations. The dependency graph is only known once constructors run, so a missing dependency of a lazy service surfaces on first resolution, or at Start if the service is eager; there is no whole-graph validation. Transient instances are not tracked and get no lifecycle hooks. A teardown that Stop handed off, because a start step was in flight, may finish just after Stop returns; the same is true of a build that completed after the scope stopped and undid itself.

Performance

benchmarks/ is a separate module comparing this package with samber/do on the same four-service graph, so the library itself stays dependency-free. On Apple Silicon:

Warm resolve Cold register and build
di 53 ns, 2 allocs 3.5 µs, 64 allocs
do v2.1 127 ns, 6 allocs 6.2 µs, 120 allocs
cd benchmarks && go test -bench . -benchmem

Versioning

While the major version is 0, a minor bump may change behaviour. Every entry in CHANGELOG.md and on the releases page says whether an upgrade can break a caller.

Contributing

Alongside ordinary tests, three generative suites guard the parts that proved easiest to get wrong: a property test over random registration sequences, a model-based test over random sequences of operations checked against documented invariants, and a fuzz target over the same invariants.

go test -race ./...
go test -run '^$' -fuzz FuzzMachine -fuzztime 2m .

The code blocks in this README are embedded from examples/ with embedmd. After editing an example:

gofmt -w examples/ && go run github.com/campoy/embedmd@v1.0.0 -w README.md

License

MIT

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

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrNotProvided = errors.New("not provided")
	ErrCycle       = errors.New("dependency cycle")
	ErrUnhealthy   = errors.New("unhealthy")
	ErrStopped     = errors.New("scope stopped")
)

Functions

func SlogObserver

func SlogObserver(l *slog.Logger) func(Event)

SlogObserver returns an observer that logs every event to l: failures at Error level, everything else at Debug.

func WithScope

func WithScope(ctx context.Context, s *Scope) context.Context

WithScope attaches s to ctx so handlers and their callees can reach it with FromContext.

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

func (b Binding[T]) Eager() Binding[T]

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

func (b Binding[T]) Health(f func(context.Context, T) error) Binding[T]

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]) Named

func (b Binding[T]) Named(name string) Binding[T]

func (Binding[T]) OnDrain added in v0.4.0

func (b Binding[T]) OnDrain(f func(context.Context, T) error) Binding[T]

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

func (b Binding[T]) OnStart(f func(context.Context, T) error) Binding[T]

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]) OnStop

func (b Binding[T]) OnStop(f func(context.Context, T) error) Binding[T]

func (Binding[T]) Run

func (b Binding[T]) Run(f func(context.Context, T) error) Binding[T]

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

func (b Binding[T]) Scoped() Binding[T]

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]) Transient

func (b Binding[T]) Transient() Binding[T]

Transient builds a new instance on every resolution, in the scope that resolves it. Instances are not tracked, so lifecycle hooks and Eager are rejected on a transient binding: it must own its own cleanup.

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).

func Named

func Named[T any](name string) Key[T]

type RunOption

type RunOption func(*runConfig)

RunOption configures Run.

func Signals

func Signals(sig ...os.Signal) RunOption

Signals replaces the signals that make Run exit. The default is os.Interrupt and SIGTERM.

func StopTimeout

func StopTimeout(d time.Duration) RunOption

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

func FromContext(ctx context.Context) (*Scope, bool)

FromContext returns the scope attached with WithScope, if any.

func New

func New() *Scope

func Test added in v0.2.0

func Test(tb TB, wire ...func(*Scope)) *Scope

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) Add

func (s *Scope) Add[T any](ctor func(*Scope) T) Binding[T]

Add appends to the multi-binding group for T; read back with s.All[T]().

func (*Scope) All

func (s *Scope) All[T any]() []T

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

func (s *Scope) Bind[I, T any]() Binding[I]

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

func (s *Scope) Child(name string) *Scope

Child creates a scope that resolves through s. Stopping s stops its children first.

func (*Scope) Context

func (s *Scope) Context() context.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

func (s *Scope) Get[T any]() T

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

func (s *Scope) HealthCheck(ctx context.Context) error

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) Lookup

func (s *Scope) Lookup[T any](k Key[T]) T

Lookup resolves a named key: s.Lookup(di.Named[*DB]("replica")).

func (*Scope) Maybe

func (s *Scope) Maybe[T any]() (T, bool)

Maybe resolves T if it is provided anywhere in the scope chain.

func (*Scope) Middleware

func (s *Scope) Middleware(next http.Handler) http.Handler

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

func (s *Scope) Must[T any](v T, err error) T

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

func (s *Scope) Observe(fn func(Event))

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

func (s *Scope) Provide[T any](ctor func(*Scope) T) Binding[T]

Provide registers a lazily built singleton. T is inferred from the constructor's return type; dependencies are pulled with s.Get[...]().

func (*Scope) Resolve

func (s *Scope) Resolve[T any]() (v T, err error)

Resolve is the error-returning entry point.

func (*Scope) Run

func (s *Scope) Run(ctx context.Context, opts ...RunOption) error

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

func (s *Scope) Shutdown(cause error)

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

func (s *Scope) Start(ctx context.Context) error

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

func (s *Scope) Stop(ctx context.Context) error

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.

func (*Scope) Value

func (s *Scope) Value[T any](v T) Binding[T]

Value registers an already-built instance.

type TB added in v0.2.0

type TB interface {
	Helper()
	Cleanup(func())
	Errorf(format string, args ...any)
}

TB is the subset of testing.TB that Test needs.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL