di

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Sep 3, 2026 License: MIT Imports: 14 Imported by: 0

README

di

CI Go Reference

A small dependency-injection container for Go 1.27+, built on generic methods. Services are registered with s.Provide(...) and resolved with s.Get[T](). No reflection over your constructors, no code generation, no dependencies.

Status: early. The API is small on purpose and may still change.

Why another one

Go 1.27 added generic methods. Before that, a typed container had to expose package-level functions such as do.Invoke[T](injector), and every variation (named, transient, must, with-context) became another function. With generic methods the whole API fits on one concrete type and reads left to right:

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

Beyond the syntax, the design makes a few deliberate choices:

  • Keys are Go types, not strings. Services are keyed by reflect.Type identity plus an optional name, so same-named types in different packages never collide and there is no naming scheme to learn.
  • Constructors return T, not (T, error). Inside a constructor, s.Get[X]() either returns the dependency or unwinds to the enclosing Resolve/Start call, which reports a normal error with the full path and the file:line where the failing service was registered.
  • Typed lifecycle hooks. OnStart/OnStop take func(context.Context, T) error on the binding. Nothing is discovered by sniffing interfaces.
  • Deterministic shutdown. Stop runs hooks in reverse build order, sequentially, and joins every error.
  • Overrides are the test seam. The last registration of a key wins, so a test wires the production graph into a fresh scope and re-registers the one thing it wants faked. No mocks of the container are needed.

Install

go get github.com/floatdrop/di

Requires Go 1.27 or newer.

Quick start

Every snippet below lives under examples/ and is compiled 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)
}

Registration

Every registration returns a typed Binding[T] whose methods refine it. Call them before the scope is first resolved; afterwards they panic.

Call Meaning
s.Provide(func(*di.Scope) T) Lazily built singleton. T is inferred from the constructor.
s.Value(v) An instance you already have.
s.Bind[I, T]() Serve requests for interface I from T's binding. Checked at registration.
s.Add(func(*di.Scope) T) Append to the multi-binding group for T.
.Named("replica") Register under a name in addition to the type.
.Transient() Build a fresh instance on every resolution.
.Scoped() One instance per resolving scope, built and stopped there. Not allowed on Value.
.Eager() Build during Start.
.OnStart(f) / .OnStop(f) Typed lifecycle hooks, f is func(context.Context, T) error.
.Run(f) Long-running function for T, run in its own goroutine and cancelled on stop.
.Health(f) Health check for T, run by HealthCheck.

Later registrations of the same key override earlier ones, which is how a child scope shadows its parent.

Resolution

Call Meaning
s.Get[T]() Resolve T. Inside a constructor a failure unwinds to the caller; at top level it panics with the error.
s.Resolve[T]() Same, returning (T, error).
s.Lookup(di.Named[T]("replica")) Resolve 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) Inside a constructor: unwrap a (T, error) pair, aborting on error. db := s.Must(sql.Open(...))
s.Context() The context passed to Start/Run, so constructors can dial with a deadline.
s.Observe(fn) Receive lifecycle events from this scope and its descendants.

Errors wrap di.ErrNotProvided or di.ErrCycle and read like:

di: building *app.Repo (provided at /src/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

// 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 child resolves through its parent, reuses the parent's singletons, and owns the lifecycle of whatever it builds itself. A singleton always builds its dependencies in the scope that registered it, so a child cannot rewire a parent singleton. A service that must see child-scoped values is declared Scoped(): one instance per resolving scope, built there.

For tests, wire the production graph into a fresh scope and override before anything is resolved:

package app

import (
	"context"
	"testing"

	"github.com/floatdrop/di"
)

func TestRepo(t *testing.T) {
	s := di.New()
	Wire(s)
	s.Value(&DB{DSN: "sqlite://memory"}) // later registration wins: replaces the production *DB
	t.Cleanup(func() { _ = s.Stop(context.Background()) })

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

Lifecycle

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 and Start returns both errors. A service built after Start, lazily or in a child scope, runs its OnStart as part of being built, so nothing is ever handed out unstarted; if that hook fails the resolution fails.

Stop stops child scopes first, then runs OnStop hooks in reverse build order and returns all failures joined with errors.Join. Only services that were actually built are stopped. Stopping a child scope also detaches it from its parent, which is what releases a per-request scope. A stopped scope, and any scope under it, refuses to resolve anything with di.ErrStopped, so a closed service is never handed out.

Group members registered with Add are ordinary bindings: singletons by default, built once, started and stopped like everything else.

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

Background workers

A binding's Run hook 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 starts in its own goroutine when the service starts. Its context is cancelled when the service stops, in reverse build order, and Stop waits for it to return within the stop deadline. Returning a non-nil error before that calls Shutdown with it, so a worker that dies takes the application down instead of leaving it half alive. http.Server does not take a context, so it keeps using OnStart and OnStop.

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 every Health hook of the services already built in the scope and its descendants, concurrently, bounded by the context, and returns the failures joined. Each failure wraps di.ErrUnhealthy and names the service. Services that were never built are not checked.

Request scopes

Middleware creates a child scope per request, registers the *http.Request in it, 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]() // built against this request's *http.Request
})

Services that depend on the request are declared once, in the root, with Scoped():

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

A scoped binding is built in the scope that resolves it, so it sees that request's *http.Request, is cached for the rest of the request, and is stopped with it. Resolving it from the root fails with ErrNotProvided rather than producing an instance built with the wrong data. di.WithScope and di.FromContext are the primitives if you are not using net/http.

The complete pattern, with a worker, a health endpoint, request scopes, and graceful shutdown, is in examples/app.

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 registered on a scope receive an Event for every constructor, OnStart, OnStop, and Health hook that runs in that scope or any scope under it, plus one for each Shutdown call. Each event names the service, its scope, the registration site, the duration, and the error if the step failed. This is also how a request scope's stop errors reach you: the middleware cannot return them, so it reports them here.

Graceful shutdown

Run is the main-function helper: it starts the scope, blocks until the context is cancelled, SIGINT/SIGTERM arrives, or something calls Shutdown, then stops everything with a bounded context (15 seconds by default, see di.StopTimeout). A second signal during the stop cancels that context so a hung hook cannot keep the process alive.

Shutdown(err) can be called from any goroutine, never blocks, and the first call wins. Use it when a long-running service dies on its own; Run returns the error you passed.

// 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
// OnStop calls http.Server.Shutdown, which stops accepting connections and
// waits for in-flight requests until the stop context expires.
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
		}).
		OnStop(func(ctx context.Context, srv *http.Server) error {
			log.Println("draining")
			return srv.Shutdown(ctx) // waits for in-flight requests, bounded by StopTimeout
		})

	// 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)
	}
}

Concurrency

Resolution is safe to call from many goroutines. Each singleton is built at most once; the per-call resolution path is kept off the scope so concurrent resolutions never share state.

Benchmarks

The benchmarks/ directory is a separate module comparing this package with samber/do on the same four-service graph. Indicative numbers on Apple Silicon:

Warm resolve Allocs Cold register + build
di 42 ns 3 2.5 µs, 46 allocs
do v2.1 119 ns 6 6.1 µs, 120 allocs
cd benchmarks && go test -bench . -benchmem

Contributing

README code blocks are generated from examples/ with embedmd. After editing an example run:

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

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 yet.
  • Generic methods cannot live on interfaces, so *di.Scope is a concrete type. Use child scopes rather than mocks to substitute dependencies.
  • Generic methods are invisible to reflect; the container does not rely on that, but tooling that discovers methods reflectively will not see them.

License

MIT

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

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]

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

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

Typed lifecycle hooks: no interface sniffing, no reflection.

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

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]

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

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 (*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. Both parameters are explicit: s.Bind[Reader, *Repo]().

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.

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.

func (*Scope) Shutdown

func (s *Scope) Shutdown(err error)

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

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

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

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

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.

func (*Scope) Value

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

Value registers an already-built instance.

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