This page is one application, read top to bottom: a small HTTP service with a database, a
cache in front of it, a mailer running in the background, and a handler built per request.
Every code block is a file from examples/guide in the repository. The Go toolchain compiles and tests them on every change, so what you
see is what runs.
1The shape of an application
Each package owns its services and exposes one function, Module, that
registers them. Nothing else about a package is special: constructors are plain
functions, types are plain types, and the only file that knows the whole graph is main.
cmd/api/main.go composes the modules, checks the graph, runs internal/config/ settings, registered as a value internal/storage/ the database, and the store built on it internal/cache/ a cache wrapped around the store internal/mail/ a background worker internal/api/ the HTTP server and its handlers
2Constructors and a module
newDB and newPGStore take what they need as parameters and
return what they make; newDB can fail. Neither imports the container.
Nothing in this application needs the general form, Provide, which takes
a closure over the scope; the two mix freely when something does.
Module hands them over with Wire. The type argument is the
key the service is served under: *db for the connection, and the Store interface for the store, since a *pgStore is assignable
to it. The parameters of a wired constructor are its dependencies, which is how the
container knows the graph before anything is built. Hooks are typed on the value they
receive and run when the application starts and stops.
Privacy is Go's. Keys are types, so *db, which only this package can
name, is a service only this package can resolve. The package exports its contract, Store and User, and its Module; the connection
has a lifecycle the container runs and is otherwise nobody else's business.
// Package storage owns the database connection and the store built on it.
// It exports its contract, Store and User, and its Module; the connection
// and the implementation are private. Keys are types, so a type only this
// package can name is a service only this package can resolve.
package storage
import (
"context"
"errors"
"github.com/floatdrop/di"
"github.com/floatdrop/di/examples/guide/internal/config"
)
// db is the database connection: private to the package, with a lifecycle
// the container runs. Its constructor takes what it needs as parameters and
// knows nothing about the container.
type db struct{ dsn string }
func newDB(cfg config.Config) (*db, error) {
if cfg.DSN == "" {
return nil, errors.New("storage: DSN is empty")
}
return &db{dsn: cfg.DSN}, nil
}
func (db *db) Ping(context.Context) error { return nil }
func (db *db) Close() error { return nil }
// Store is what the rest of the application depends on. Handlers take the
// interface; the container serves whatever is registered for it.
type Store interface {
Find(ctx context.Context, id string) (User, error)
Ping(ctx context.Context) error
}
type User struct{ ID, Name string }
type pgStore struct{ db *db }
func newPGStore(db *db) *pgStore { return &pgStore{db: db} }
func (s *pgStore) Find(_ context.Context, id string) (User, error) {
return User{ID: id, Name: "user " + id + " via " + s.db.dsn}, nil
}
func (s *pgStore) Ping(ctx context.Context) error { return s.db.Ping(ctx) }
// Module registers the package's services. Constructors are handed over as
// they are; the hooks are typed on what they receive. The Store key is
// served by the private constructor, whose result is assignable to it.
func Module(s *di.Scope) {
s.Wire[*db](newDB).
OnStart(func(ctx context.Context, db *db) error { return db.Ping(ctx) }).
OnStop(func(_ context.Context, db *db) error { return db.Close() })
s.Wire[Store](newPGStore)
}3Configuration is a value
A value you already have is registered with Value. It is a key like any
other: NewDB receives it as a parameter, and a test replaces it with Override() and every service downstream follows.
// Package config reads the settings the application starts with.
package config
import (
"os"
"github.com/floatdrop/di"
)
type Config struct {
Addr string
DSN string
}
func Load() Config {
return Config{
Addr: env("ADDR", ":8080"),
DSN: env("DSN", "postgres://localhost/app"),
}
}
func env(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
// Module registers the configuration as a value. A test overrides it with
// s.Value(config.Config{...}).Override() and everything downstream follows.
func Module(s *di.Scope) { s.Value(Load()) }4Composition in main
Use applies the modules in order and attributes each registration to the
module that made it, which is what an error names when two modules collide. A second
registration of a key without Override() is rejected, naming both, so one
module cannot rewire another unnoticed.
Validate walks the declared graph without building anything, told what a
request scope will hold. A dependency nothing provides, a cycle, or a request-scoped
service captured by a singleton fails here, at startup, rather than on the first
request. Then Run starts the eager services, waits for a signal, and stops everything
in reverse order within the timeout.
// Command api is the application: it composes the modules, checks the graph,
// and runs until a signal arrives.
package main
import (
"context"
"log"
"net/http"
"time"
"github.com/floatdrop/di"
"github.com/floatdrop/di/dihttp"
"github.com/floatdrop/di/examples/guide/internal/api"
"github.com/floatdrop/di/examples/guide/internal/cache"
"github.com/floatdrop/di/examples/guide/internal/config"
"github.com/floatdrop/di/examples/guide/internal/mail"
"github.com/floatdrop/di/examples/guide/internal/storage"
)
func main() {
app := di.New()
app.Use(config.Module, storage.Module, cache.Module, mail.Module, dihttp.Module, api.Module)
// Nothing has been built yet. The constructors declared their
// dependencies, so the graph is checked here, as a request scope holding
// an *http.Request would resolve it.
if err := app.Validate(di.Provided[*http.Request]()).Err(); err != nil {
log.Fatal(err)
}
// Run starts the eager services and their hooks, waits for SIGINT or
// SIGTERM, then stops everything in reverse order within the timeout.
if err := app.Run(context.Background(), di.StopTimeout(10*time.Second)); err != nil {
log.Fatal(err)
}
}5Background workers
A Worker runs for as long as its service does: started in its own goroutine
when the service starts, cancelled by Stop, and waited for before anything
it depends on is torn down. Returning an error from it stops the application. Eager says the mailer exists by the time Start returns rather
than on first use.
// Package mail sends messages from a background worker.
package mail
import (
"context"
"log"
"github.com/floatdrop/di"
)
type Mailer struct{ queue chan string }
func New() *Mailer { return &Mailer{queue: make(chan string, 64)} }
// Send queues a message; the worker delivers it.
func (m *Mailer) Send(msg string) {
select {
case m.queue <- msg:
default:
log.Println("mail: queue full, dropped", msg)
}
}
// Run delivers until ctx is cancelled, which Stop does before the services
// the mailer depends on are stopped.
func (m *Mailer) Run(ctx context.Context) error {
for {
select {
case msg := <-m.queue:
log.Println("mail: sent", msg)
case <-ctx.Done():
return nil
}
}
}
// Module registers the mailer as an eager service with a worker: it exists
// once Start returns, its loop runs in its own goroutine, and Stop cancels
// the loop and waits for it.
func Module(s *di.Scope) {
s.Wire[*Mailer](New).
Eager().
Worker(func(ctx context.Context, m *Mailer) error { return m.Run(ctx) })
}6HTTP and request scopes
A dihttp.Middleware opens a child scope for each request, holding the *http.Request. Services declared Scoped in the application
scope are built once per request scope, from singletons and request-scoped values
alike, and stopped with it. A handler reaches its scope through di.FromContext, or through dihttp.Handle, which does that for
a handler type's method.
A handler type covers one resource, with a method per route, so its dependencies are
declared once. dihttp.Handle((*Users).Show) resolves the type from the
request scope and calls the method; a method expression names both, so no type
argument is needed. Users is Scoped because it needs the
caller; Health needs nothing from the request and is an ordinary
singleton, and Handle follows either lifetime.
The middleware needs the scope itself, to open a child per request, so dihttp.Module registers it as a service and the server takes it as a
parameter like anything else. OnDrain runs before anything is stopped, so
requests in flight keep their scopes while http.Server.Shutdown waits for
them.
// Package api serves HTTP. The server is a singleton with a lifecycle. A
// handler type covers one resource, with a method per route; it is Scoped
// when it needs the request, and built in the request scope the dihttp
// middleware opens, or a plain singleton when it does not.
package api
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"github.com/floatdrop/di"
"github.com/floatdrop/di/dihttp"
"github.com/floatdrop/di/examples/guide/internal/config"
"github.com/floatdrop/di/examples/guide/internal/mail"
"github.com/floatdrop/di/examples/guide/internal/storage"
)
// Caller is who is making the request. It depends on the *http.Request, which
// only a request scope provides.
type Caller struct{ Name string }
func NewCaller(r *http.Request) *Caller { return &Caller{Name: r.Header.Get("X-User")} }
// Users is built once per request, from singletons and request-scoped values
// alike, and serves every route about users.
type Users struct {
store storage.Store
mail *mail.Mailer
caller *Caller
}
func NewUsers(store storage.Store, m *mail.Mailer, caller *Caller) *Users {
return &Users{store: store, mail: m, caller: caller}
}
func (u *Users) Show(w http.ResponseWriter, r *http.Request) {
user, err := u.store.Find(r.Context(), r.PathValue("id"))
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
fmt.Fprintln(w, user.Name)
}
func (u *Users) Greet(w http.ResponseWriter, r *http.Request) {
user, err := u.store.Find(r.Context(), r.PathValue("id"))
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
u.mail.Send(u.caller.Name + " greets " + user.Name)
w.WriteHeader(http.StatusAccepted)
}
// Health needs nothing from the request, so it is an ordinary singleton. It
// asks the store, which is the storage package's contract; the connection
// behind it is that package's own business.
type Health struct{ store storage.Store }
func NewHealth(store storage.Store) *Health { return &Health{store: store} }
func (h *Health) Check(w http.ResponseWriter, r *http.Request) {
if err := h.store.Ping(r.Context()); err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
fmt.Fprintln(w, "ok")
}
// NewServer builds the routes. Each one resolves its handler from the
// request scope the middleware opens; dihttp.Module provides the middleware.
func NewServer(cfg config.Config, mw dihttp.Middleware) *http.Server {
mux := http.NewServeMux()
mux.Handle("GET /users/{id}", dihttp.Handle((*Users).Show))
mux.Handle("POST /users/{id}/greet", dihttp.Handle((*Users).Greet))
mux.Handle("GET /healthz", dihttp.Handle((*Health).Check))
return &http.Server{Addr: cfg.Addr, Handler: mw(mux)}
}
// Module registers the request-scoped values, the handlers and the server,
// with the hooks that bind, drain and close it.
func Module(s *di.Scope) {
s.Wire[*Caller](NewCaller).Scoped()
s.Wire[*Users](NewUsers).Scoped()
s.Wire[*Health](NewHealth)
s.Wire[*http.Server](NewServer).
Eager().
OnStart(func(_ context.Context, srv *http.Server) error {
// Bind synchronously, so a busy port fails Start; serve in the
// background, and take the application down if serving stops.
ln, err := net.Listen("tcp", srv.Addr)
if err != nil {
return err
}
go func() {
if err := srv.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
s.Shutdown(err)
}
}()
return nil
}).
// Draining runs before anything is stopped, so requests still in
// flight keep their scopes and everything those depend on.
OnDrain(func(ctx context.Context, srv *http.Server) error { return srv.Shutdown(ctx) }).
OnStop(func(_ context.Context, srv *http.Server) error { return srv.Close() })
}7Wrapping without replacing
Wrap composes over whatever serves a key. The wrapper takes that value
first and its other dependencies after it. The store keeps its registration and its
hooks, is built first, and is stopped after the wrapper, and the wrapper forwards what
it does not change. The one thing to get right is module order: the cache's module
comes after storage's. A wrapper registered in a child scope applies to that scope and
its descendants only. Like storage, this package exports only its Module:
a cross-cutting concern composes over an exported contract, never over a package's
internals.
// Package cache puts a cache in front of the store. It replaces nothing:
// the store keeps its registration and hooks, and this wraps it. The package
// exports only its Module.
package cache
import (
"context"
"sync"
"github.com/floatdrop/di"
"github.com/floatdrop/di/examples/guide/internal/storage"
)
type cache struct {
mu sync.Mutex
users map[string]storage.User
hits int
}
func newCache() *cache { return &cache{users: map[string]storage.User{}} }
// cachingStore is a Store that asks the one it wraps only on a miss, and
// forwards what it does not change.
type cachingStore struct {
next storage.Store
cache *cache
}
// newCachingStore takes the store it wraps first, then its dependencies.
func newCachingStore(next storage.Store, c *cache) storage.Store {
return &cachingStore{next: next, cache: c}
}
func (s *cachingStore) Find(ctx context.Context, id string) (storage.User, error) {
s.cache.mu.Lock()
user, ok := s.cache.users[id]
if ok {
s.cache.hits++
}
s.cache.mu.Unlock()
if ok {
return user, nil
}
user, err := s.next.Find(ctx, id)
if err != nil {
return user, err
}
s.cache.mu.Lock()
s.cache.users[id] = user
s.cache.mu.Unlock()
return user, nil
}
func (s *cachingStore) Ping(ctx context.Context) error { return s.next.Ping(ctx) }
// Module registers the cache and wraps whatever serves Store by now. Order
// matters: this module comes after storage's.
func Module(s *di.Scope) {
s.Wire[*cache](newCache)
s.Wrap[storage.Store](newCachingStore)
}8Testing by overriding
di.Test wires the modules into a fresh scope and stops it when the test
ends. Overriding the configuration is enough to point the store at another database;
a fake would be s.Value(&fake).Override() just the same. The marker is
required: a second registration without it is rejected, so a test cannot pass against
production wiring by accident.
package storage_test
import (
"context"
"strings"
"testing"
"github.com/floatdrop/di"
"github.com/floatdrop/di/examples/guide/internal/config"
"github.com/floatdrop/di/examples/guide/internal/storage"
)
// The production modules, with the configuration overridden: the store is
// built against a database that dials the test DSN, and nothing else in the
// wiring changes.
func TestStoreFindsUsers(t *testing.T) {
s := di.Test(t, config.Module, storage.Module)
s.Value(config.Config{DSN: "sqlite://memory"}).Override()
user, err := s.Get[storage.Store]().Find(context.Background(), "42")
if err != nil {
t.Fatal(err)
}
if !strings.Contains(user.Name, "sqlite://memory") {
t.Fatalf("store was built against the wrong database: %q", user.Name)
}
}The wrapper is tested through the same modules, with nothing faked: two lookups, one hit. It is an internal test, because only the cache package can name its own cache.
package cache
import (
"context"
"testing"
"github.com/floatdrop/di"
"github.com/floatdrop/di/examples/guide/internal/config"
"github.com/floatdrop/di/examples/guide/internal/storage"
)
// An internal test, since the cache is private: only this package can name
// *cache, so only this package can read its hits.
func TestSecondLookupIsAHit(t *testing.T) {
s := di.Test(t, config.Module, storage.Module, Module)
store := s.Get[storage.Store]()
for range 2 {
if _, err := store.Find(context.Background(), "42"); err != nil {
t.Fatal(err)
}
}
if hits := s.Get[*cache]().hits; hits != 1 {
t.Fatalf("want one hit, got %d", hits)
}
}9Seeing the graph
Before anything is built, Explain draws what the wired constructors
declared: dashed edges, the wrapper over the store, and which service declares the one
you asked about. After a build it draws what actually happened, solid, followed by what
needed it. Graph renders the whole application as Graphviz DOT. This is app.Explain[storage.Store]() at startup, pinned by a test in the
repository.
storage.Store: singleton wrapper in root, not built (provided at internal/cache/cache.go:60)
├╌╌ storage.Store: singleton in root, not built (provided at internal/storage/storage.go:56)
│ └╌╌ *storage.db: singleton in root, not built (provided at internal/storage/storage.go:53)
│ └╌╌ config.Config: value in root, not built (provided at internal/config/config.go:31)
└╌╌ *cache.cache: singleton in root, not built (provided at internal/cache/cache.go:59)
declared by: *api.Users in root, *api.Health in rootModules is the same information read by module rather than by service: what
each provides, what it needs and who serves it, what it wraps, and which of its
constructors are closures. It is derived from the registrations, so there is no
manifest to keep in step. This is the whole application, before anything is built,
pinned by a test as well.
config.Module
provides config.Config
storage.Module
provides *storage.db, storage.Store
needs config.Config ← config.Module
cache.Module
provides *cache.cache
wraps storage.Store ← storage.Module
mail.Module
provides *mail.Mailer
dihttp.Module
provides dihttp.Middleware
unchecked dihttp.Middleware (closures: needs known when they run)
api.Module
provides *api.Caller, *api.Users, *api.Health, *http.Server
needs *http.Request ← owed to a resolving scope
storage.Store ← cache.Module
*mail.Mailer ← mail.Module
config.Config ← config.Module
dihttp.Middleware ← dihttp.Module10Run it
Ctrl-C drains the server, cancels the mailer, closes the database, in that order, and reports any hook that failed.
git clone https://github.com/floatdrop/di && cd di
go run ./examples/guide/cmd/api &
curl -H 'X-User: ada' localhost:8080/users/42The README covers the rest: groups, observers, and the
rules the container enforces. How it works goes the other way, from Get to a value: lifetimes, phases, cycles and shutdown, with diagrams.