di API

di

package

API reference for the di package.

S
struct

testDB

app/di/container_test.go:16-18
type testDB struct

Fields

Name Type Description
Name string
S
struct

testService

app/di/container_test.go:20-23
type testService struct

Fields

Name Type Description
DB *testDB inject:"db"
Logger string inject:"logger"
F
function

TestContainer_ProvideAndGet

Parameters

app/di/container_test.go:25-43
func TestContainer_ProvideAndGet(t *testing.T)

{
	c := New()
	db := &testDB{Name: "test"}
	c.Provide("db", db)

	got, ok := c.Get("db")
	if !ok {
		t.Fatal("expected to find 'db'")
	}

	gotDB, ok := got.(*testDB)
	if !ok {
		t.Fatal("expected *testDB type")
	}

	if gotDB.Name != "test" {
		t.Errorf("got %q, want %q", gotDB.Name, "test")
	}
}
F
function

TestContainer_Has

Parameters

app/di/container_test.go:45-56
func TestContainer_Has(t *testing.T)

{
	c := New()
	c.Provide("exists", "value")

	if !c.Has("exists") {
		t.Error("Has should return true for 'exists'")
	}

	if c.Has("missing") {
		t.Error("Has should return false for 'missing'")
	}
}
F
function

TestContainer_Inject

Parameters

app/di/container_test.go:58-76
func TestContainer_Inject(t *testing.T)

{
	c := New()
	db := &testDB{Name: "injected"}
	c.Provide("db", db)
	c.Provide("logger", "stdout")

	svc := &testService{}
	if err := c.Inject(svc); err != nil {
		t.Fatalf("Inject() error = %v", err)
	}

	if svc.DB != db {
		t.Errorf("DB not injected correctly")
	}

	if svc.Logger != "stdout" {
		t.Errorf("Logger: got %q, want %q", svc.Logger, "stdout")
	}
}
F
function

TestContainer_InjectRejectsMissingDependency

Parameters

app/di/container_test.go:78-86
func TestContainer_InjectRejectsMissingDependency(t *testing.T)

{
	c := New()
	c.Provide("db", &testDB{})

	err := c.Inject(&testService{})
	if err == nil {
		t.Fatal("Inject() accepted a missing dependency")
	}
}
F
function

TestContainer_InjectRejectsWrongType

Parameters

app/di/container_test.go:88-97
func TestContainer_InjectRejectsWrongType(t *testing.T)

{
	c := New()
	c.Provide("db", &testDB{})
	c.Provide("logger", 42)

	err := c.Inject(&testService{})
	if err == nil {
		t.Fatal("Inject() accepted a dependency with the wrong type")
	}
}
F
function

TestContainer_InjectRejectsUnexportedField

Parameters

app/di/container_test.go:99-109
func TestContainer_InjectRejectsUnexportedField(t *testing.T)

{
	target := struct {
		value string `inject:"value"`
	}{}
	container := NewBuilder()
	container.Provide("value", "ok")

	if err := container.MustBuild().Inject(&target); err == nil {
		t.Fatal("Inject() accepted an unexported field")
	}
}
F
function

TestContainer_Clone

Parameters

app/di/container_test.go:111-129
func TestContainer_Clone(t *testing.T)

{
	c := New()
	c.Provide("key", "value")

	clone := c.Clone()
	clone.Provide("new", "added")

	if !clone.Has("key") {
		t.Error("clone should have 'key'")
	}

	if !clone.Has("new") {
		t.Error("clone should have 'new'")
	}

	if c.Has("new") {
		t.Error("original should not have 'new'")
	}
}
F
function

TestContainer_Keys

Parameters

app/di/container_test.go:131-140
func TestContainer_Keys(t *testing.T)

{
	c := New()
	c.Provide("a", 1)
	c.Provide("b", 2)

	keys := c.Keys()
	if len(keys) != 2 {
		t.Errorf("got %d keys, want 2", len(keys))
	}
}
F
function

TestContainer_MustGet_Panic

Parameters

app/di/container_test.go:142-152
func TestContainer_MustGet_Panic(t *testing.T)

{
	c := New()

	defer func() {
		if r := recover(); r == nil {
			t.Error("MustGet should panic for missing key")
		}
	}()

	c.MustGet("missing")
}
F
function

TestResolve

Parameters

app/di/container_test.go:154-165
func TestResolve(t *testing.T)

{
	c := New()
	c.Provide("num", 42)

	got, ok := Resolve[int](c, "num")
	if !ok {
		t.Fatal("expected to resolve 'num'")
	}
	if got != 42 {
		t.Errorf("got %d, want 42", got)
	}
}
F
function

TestResolve_TypeMismatch

Parameters

app/di/container_test.go:167-175
func TestResolve_TypeMismatch(t *testing.T)

{
	c := New()
	c.Provide("num", 42)

	_, ok := Resolve[string](c, "num")
	if ok {
		t.Error("should return false for type mismatch")
	}
}
F
function

TestMustResolve_Panic

Parameters

app/di/container_test.go:177-187
func TestMustResolve_Panic(t *testing.T)

{
	c := New()

	defer func() {
		if r := recover(); r == nil {
			t.Error("MustResolve should panic for missing key")
		}
	}()

	MustResolve[int](c, "missing")
}
F
function

TestBuilder_RegisterAndResolveType

Parameters

app/di/container_test.go:189-198
func TestBuilder_RegisterAndResolveType(t *testing.T)

{
	b := NewBuilder()
	Register(b, func() *testDB { return &testDB{Name: "built"} })

	c := b.MustBuild()
	db := ResolveType[*testDB](c)
	if db.Name != "built" {
		t.Errorf("got %q, want %q", db.Name, "built")
	}
}
F
function

TestBuilder_RegisterAs

Parameters

app/di/container_test.go:200-208
func TestBuilder_RegisterAs(t *testing.T)

{
	b := NewBuilder()
	RegisterAs[Worker](b, func() *GoodWorker { return &GoodWorker{} })

	c := b.MustBuild()
	if got := ResolveType[Worker](c).Work(); got != "working hard" {
		t.Fatalf("ResolveType[Worker]() = %q", got)
	}
}
F
function

TestBuilder_RegisterImplRejectsInvalidContract

Parameters

app/di/container_test.go:210-217
func TestBuilder_RegisterImplRejectsInvalidContract(t *testing.T)

{
	b := NewBuilder()
	RegisterImpl[Worker, *BrokenWorker](b)

	if _, err := b.Build(); err == nil {
		t.Fatal("Build() accepted an invalid implementation")
	}
}
F
function

TestBuilder_RegisterImplIgnoresContractMarkerForInjection

Parameters

app/di/container_test.go:219-230
func TestBuilder_RegisterImplIgnoresContractMarkerForInjection(t *testing.T)

{
	b := NewBuilder()
	RegisterImpl[Worker, *GoodWorker](b)

	worker, err := TryResolveType[Worker](b.MustBuild())
	if err != nil {
		t.Fatal(err)
	}
	if worker.Work() != "working hard" {
		t.Fatalf("Work() = %q", worker.Work())
	}
}
S
struct
Implements: Worker

configuredWorker

app/di/container_test.go:232-234
type configuredWorker struct

Methods

Work
Method

Returns

string
func (*configuredWorker) Work() string
{
	return w.Config.DSN
}

Fields

Name Type Description
Config *Config inject:"config"
F
function

TestBuilder_RegisterImplValidatesPointerFields

Parameters

app/di/container_test.go:240-247
func TestBuilder_RegisterImplValidatesPointerFields(t *testing.T)

{
	b := NewBuilder()
	RegisterImpl[Worker, *configuredWorker](b)

	if _, err := b.Build(); err == nil {
		t.Fatal("Build() accepted a pointer implementation with a missing dependency")
	}
}
S
struct
Implements: Worker

hiddenDependencyWorker

app/di/container_test.go:249-251
type hiddenDependencyWorker struct

Methods

Work
Method

Returns

string
func (*hiddenDependencyWorker) Work() string
{
	return w.value
}

Fields

Name Type Description
value string inject:"value"
F
function

TestBuilder_RegisterImplRejectsUnexportedInjectionField

Parameters

app/di/container_test.go:257-265
func TestBuilder_RegisterImplRejectsUnexportedInjectionField(t *testing.T)

{
	b := NewBuilder()
	Register(b, func() string { return "configured" })
	RegisterImpl[Worker, *hiddenDependencyWorker](b)

	if _, err := b.Build(); err == nil {
		t.Fatal("Build() accepted an unexported injection field")
	}
}
F
function

TestBuilder_TransientLifetime

Parameters

app/di/container_test.go:267-277
func TestBuilder_TransientLifetime(t *testing.T)

{
	b := NewBuilder()
	Register(b, func() *testDB { return &testDB{Name: "fresh"} }, Transient)

	c := b.MustBuild()
	a := ResolveType[*testDB](c)
	b2 := ResolveType[*testDB](c)
	if a == b2 {
		t.Error("Transient should return new instances")
	}
}
F
function

TestBuilder_SingletonLifetime

Parameters

app/di/container_test.go:279-289
func TestBuilder_SingletonLifetime(t *testing.T)

{
	b := NewBuilder()
	Register(b, func() *testDB { return &testDB{Name: "singleton"} }, Singleton)

	c := b.MustBuild()
	a := ResolveType[*testDB](c)
	b2 := ResolveType[*testDB](c)
	if a != b2 {
		t.Error("Singleton should return same instance")
	}
}
F
function

TestBuilder_ConcurrentSingletonResolution

Parameters

app/di/container_test.go:291-323
func TestBuilder_ConcurrentSingletonResolution(t *testing.T)

{
	b := NewBuilder()
	started := make(chan struct{})
	release := make(chan struct{})
	var calls atomic.Int32
	Register(b, func() *testDB {
		if calls.Add(1) == 1 {
			close(started)
		}
		<-release
		return &testDB{Name: "singleton"}
	})

	container := b.MustBuild()
	results := make(chan *testDB, 2)
	go func() {
		results <- ResolveType[*testDB](container)
	}()
	<-started
	go func() {
		results <- ResolveType[*testDB](container)
	}()
	close(release)

	first := <-results
	second := <-results
	if first != second {
		t.Fatal("concurrent singleton resolutions returned different instances")
	}
	if got := calls.Load(); got != 1 {
		t.Fatalf("singleton factory called %d times", got)
	}
}
F
function

TestResolveType_PanicOnMissing

Parameters

app/di/container_test.go:325-336
func TestResolveType_PanicOnMissing(t *testing.T)

{
	b := NewBuilder()
	c := b.MustBuild()

	defer func() {
		if r := recover(); r == nil {
			t.Error("ResolveType should panic on missing type")
		}
	}()

	ResolveType[*testDB](c)
}
F
function

TestBuilder_ProvideNamed

Parameters

app/di/container_test.go:338-351
func TestBuilder_ProvideNamed(t *testing.T)

{
	b := NewBuilder()
	b.Provide("db", &testDB{Name: "named"})

	c := b.MustBuild()
	got, ok := c.Get("db")
	if !ok {
		t.Fatal("expected named dep 'db'")
	}
	db := got.(*testDB)
	if db.Name != "named" {
		t.Errorf("got %q, want %q", db.Name, "named")
	}
}
F
function

TestBuilder_RejectsDuplicateNamedDependency

Parameters

app/di/container_test.go:353-361
func TestBuilder_RejectsDuplicateNamedDependency(t *testing.T)

{
	b := NewBuilder()
	b.Provide("db", &testDB{})
	b.Provide("db", &testDB{})

	if _, err := b.Build(); err == nil {
		t.Fatal("Build() accepted a duplicate named dependency")
	}
}
F
function

TestBuilder_RejectsNilInstances

Parameters

app/di/container_test.go:363-372
func TestBuilder_RejectsNilInstances(t *testing.T)

{
	b := NewBuilder()
	var database *testDB
	RegisterInstance(b, database)
	b.Provide("database", database)

	if _, err := b.Build(); err == nil {
		t.Fatal("Build() accepted nil instances")
	}
}
F
function

TestBuilder_RejectsNilFactories

Parameters

app/di/container_test.go:374-382
func TestBuilder_RejectsNilFactories(t *testing.T)

{
	b := NewBuilder()
	var factory func() *testDB
	Register(b, factory)

	if _, err := b.Build(); err == nil {
		t.Fatal("Build() accepted a nil factory")
	}
}
F
function

TestBuilder_RejectsNilFactoryResults

Parameters

app/di/container_test.go:384-408
func TestBuilder_RejectsNilFactoryResults(t *testing.T)

{
	t.Run("register", func(t *testing.T) {
		b := NewBuilder()
		Register(b, func() *testDB { return nil })
		if _, err := TryResolveType[*testDB](b.MustBuild()); err == nil {
			t.Fatal("TryResolveType() accepted a nil factory result")
		}
	})

	t.Run("register as", func(t *testing.T) {
		b := NewBuilder()
		RegisterAs[Worker](b, func() *GoodWorker { return nil })
		if _, err := TryResolveType[Worker](b.MustBuild()); err == nil {
			t.Fatal("TryResolveType() accepted a nil implementation")
		}
	})

	t.Run("constructor", func(t *testing.T) {
		b := NewBuilder()
		RegisterFromFunc[*testDB](b, func() *testDB { return nil })
		if _, err := TryResolveType[*testDB](b.MustBuild()); err == nil {
			t.Fatal("TryResolveType() accepted a nil constructor result")
		}
	})
}
S
struct

valueError

app/di/container_test.go:410-410
type valueError struct

Methods

Error
Method

Returns

string
func (valueError) Error() string
{
	return "value error"
}
F
function

TestRegisterFromFuncRejectsConcreteErrorResult

Parameters

app/di/container_test.go:416-425
func TestRegisterFromFuncRejectsConcreteErrorResult(t *testing.T)

{
	defer func() {
		if recover() == nil {
			t.Fatal("RegisterFromFunc() accepted a concrete error result")
		}
	}()
	RegisterFromFunc[*testDB](NewBuilder(), func() (*testDB, valueError) {
		return &testDB{}, valueError{}
	})
}
I
interface

Worker

app/di/container_test.go:427-429
type Worker interface

Methods

Work
Method

Returns

string
func Work(...)
S
struct
Implements: Worker

GoodWorker

app/di/container_test.go:431-433
type GoodWorker struct

Methods

Work
Method

Returns

string
func (*GoodWorker) Work() string
{
	return "working hard"
}
S
struct
Implements: Worker

LazyWorker

app/di/container_test.go:439-441
type LazyWorker struct

Methods

Work
Method

Returns

string
func (*LazyWorker) Work() string
{
	return "working smart"
}
S
struct

BrokenWorker

app/di/container_test.go:447-449
type BrokenWorker struct
F
function

TestContainer_ProvideWithContracts

Parameters

app/di/container_test.go:451-471
func TestContainer_ProvideWithContracts(t *testing.T)

{
	c := New()

	t.Run("Valid implementation", func(t *testing.T) {
		defer func() {
			if r := recover(); r != nil {
				t.Errorf("Provide should not panic for valid worker: %v", r)
			}
		}()
		c.Provide("good", &GoodWorker{})
	})

	t.Run("Invalid implementation panics", func(t *testing.T) {
		defer func() {
			if r := recover(); r == nil {
				t.Error("Provide should panic for broken worker")
			}
		}()
		c.Provide("broken", &BrokenWorker{})
	})
}
F
function

TestResolveAll

Parameters

app/di/container_test.go:473-498
func TestResolveAll(t *testing.T)

{
	c := New()
	c.Provide("good", &GoodWorker{})
	c.Provide("lazy", &LazyWorker{})
	c.Provide("other", "not a worker")

	workers := ResolveAll[Worker](c)
	if len(workers) != 2 {
		t.Fatalf("expected 2 workers, got %d", len(workers))
	}

	foundGood := false
	foundLazy := false
	for _, w := range workers {
		switch w.Work() {
		case "working hard":
			foundGood = true
		case "working smart":
			foundLazy = true
		}
	}

	if !foundGood || !foundLazy {
		t.Error("ResolveAll did not find all expected workers")
	}
}
F
function

TestContainer_Scope

Parameters

app/di/container_test.go:500-508
func TestContainer_Scope(t *testing.T)

{
	c := New()
	c.Provide("shared", "value")

	child := c.Scope()
	if !child.Has("shared") {
		t.Error("child should inherit named deps from parent")
	}
}
F
function

TestContainer_ScopeDoesNotOwnSingleton

Parameters

app/di/container_test.go:510-529
func TestContainer_ScopeDoesNotOwnSingleton(t *testing.T)

{
	b := NewBuilder()
	Register(b, func() *closableService { return &closableService{} }, Singleton)

	container := b.MustBuild()
	scope := container.Scope()
	service := ResolveType[*closableService](scope)
	if err := scope.Close(); err != nil {
		t.Fatalf("scope.Close(): %v", err)
	}
	if service.closed {
		t.Fatal("scope closed a singleton owned by the root container")
	}
	if err := container.Close(); err != nil {
		t.Fatalf("container.Close(): %v", err)
	}
	if !service.closed {
		t.Fatal("root container did not close its singleton")
	}
}
F
function

TestContainer_ProvideLazy

Parameters

app/di/container_test.go:531-561
func TestContainer_ProvideLazy(t *testing.T)

{
	c := New()
	called := 0
	c.ProvideLazy("lazy", func() any {
		called++
		return "computed"
	})

	if called != 0 {
		t.Error("lazy factory should not be called on registration")
	}

	v, ok := c.Get("lazy")
	if !ok {
		t.Fatal("expected to find 'lazy'")
	}
	if v != "computed" {
		t.Errorf("got %v, want %q", v, "computed")
	}
	if called != 1 {
		t.Error("lazy factory should be called once on first access")
	}

	v2, _ := c.Get("lazy")
	if v2 != "computed" {
		t.Error("lazy factory should return cached value")
	}
	if called != 1 {
		t.Error("lazy factory should only be called once")
	}
}
F
function

TestContainer_ResolveAllAllowsReentrantLazyFactory

Parameters

app/di/container_test.go:563-583
func TestContainer_ResolveAllAllowsReentrantLazyFactory(t *testing.T)

{
	c := New()
	c.ProvideLazy("worker", func() any {
		c.Provide("side-effect", "registered")
		return &GoodWorker{}
	})

	done := make(chan []Worker, 1)
	go func() {
		done <- ResolveAll[Worker](c)
	}()

	select {
	case workers := <-done:
		if len(workers) != 1 {
			t.Fatalf("ResolveAll() returned %d workers", len(workers))
		}
	case <-time.After(time.Second):
		t.Fatal("ResolveAll() deadlocked in a reentrant lazy factory")
	}
}
F
function

TestContainer_ProvideLazyRejectsNilFactory

Parameters

app/di/container_test.go:585-592
func TestContainer_ProvideLazyRejectsNilFactory(t *testing.T)

{
	defer func() {
		if recover() == nil {
			t.Fatal("ProvideLazy() accepted a nil factory")
		}
	}()
	New().ProvideLazy("nil", nil)
}
F
function

TestContainer_ProvideLazyRejectsNilResult

Parameters

app/di/container_test.go:594-602
func TestContainer_ProvideLazyRejectsNilResult(t *testing.T)

{
	container := New()
	container.ProvideLazy("nil", func() any {
		return (*testDB)(nil)
	})
	if value, ok := container.Get("nil"); ok || value != nil {
		t.Fatalf("Get() = (%v, %v), want (nil, false)", value, ok)
	}
}
F
function

TestContainer_NamedCloserIsClosed

Parameters

app/di/container_test.go:604-616
func TestContainer_NamedCloserIsClosed(t *testing.T)

{
	service := &closableService{}
	builder := NewBuilder()
	builder.Provide("service", service)
	container := builder.MustBuild()

	if err := container.Close(); err != nil {
		t.Fatal(err)
	}
	if !service.closed {
		t.Fatal("Close() did not close a named dependency")
	}
}
F
function

TestContainer_LazyCloserBelongsToRoot

Parameters

app/di/container_test.go:618-639
func TestContainer_LazyCloserBelongsToRoot(t *testing.T)

{
	service := &closableService{}
	container := New()
	container.ProvideLazy("service", func() any { return service })
	scope := container.Scope()

	if _, ok := scope.Get("service"); !ok {
		t.Fatal("scope did not resolve the lazy dependency")
	}
	if err := scope.Close(); err != nil {
		t.Fatal(err)
	}
	if service.closed {
		t.Fatal("scope closed a lazy dependency owned by the root")
	}
	if err := container.Close(); err != nil {
		t.Fatal(err)
	}
	if !service.closed {
		t.Fatal("root did not close its lazy dependency")
	}
}
F
function

TestContainerRejectsUseAfterClose

Parameters

app/di/container_test.go:641-659
func TestContainerRejectsUseAfterClose(t *testing.T)

{
	container := New()
	container.Provide("value", 1)
	if err := container.Close(); err != nil {
		t.Fatal(err)
	}
	if _, ok := container.Get("value"); ok {
		t.Fatal("Get() returned a value after Close()")
	}
	if _, err := TryResolveType[*testDB](container); !errors.Is(err, ErrContainerClosed) {
		t.Fatalf("TryResolveType() error = %v, want ErrContainerClosed", err)
	}
	defer func() {
		if recover() == nil {
			t.Fatal("Provide() succeeded after Close()")
		}
	}()
	container.Provide("late", 2)
}
F
function

TestResolveAllRejectsUseAfterClose

Parameters

app/di/container_test.go:661-678
func TestResolveAllRejectsUseAfterClose(t *testing.T)

{
	container := New()
	var called atomic.Bool
	container.ProvideLazy("worker", func() any {
		called.Store(true)
		return &GoodWorker{}
	})
	if err := container.Close(); err != nil {
		t.Fatal(err)
	}

	if workers := ResolveAll[Worker](container); len(workers) != 0 {
		t.Fatalf("ResolveAll() returned %d workers after Close()", len(workers))
	}
	if called.Load() {
		t.Fatal("ResolveAll() realized a lazy dependency after Close()")
	}
}
F
function

TestContainerProvideLinearizesWithClose

Parameters

app/di/container_test.go:680-712
func TestContainerProvideLinearizesWithClose(t *testing.T)

{
	for iteration := 0; iteration < 100; iteration++ {
		container := New()
		service := &closableService{}
		start := make(chan struct{})
		var wg sync.WaitGroup
		var provided atomic.Bool
		wg.Add(2)
		go func() {
			defer wg.Done()
			<-start
			_ = container.Close()
		}()
		go func() {
			defer wg.Done()
			<-start
			defer func() {
				_ = recover()
			}()
			container.Provide("service", service)
			provided.Store(true)
		}()
		close(start)
		wg.Wait()

		if container.Has("service") {
			t.Fatal("closed container reported a registered service")
		}
		if provided.Load() && !service.closed {
			t.Fatal("service admitted before Close() was not closed")
		}
	}
}
F
function

TestContainerClosesLazyResourceRacingClose

Parameters

app/di/container_test.go:714-740
func TestContainerClosesLazyResourceRacingClose(t *testing.T)

{
	container := New()
	started := make(chan struct{})
	release := make(chan struct{})
	service := &closableService{}
	container.ProvideLazy("service", func() any {
		close(started)
		<-release
		return service
	})
	resolved := make(chan bool, 1)
	go func() {
		_, ok := container.Get("service")
		resolved <- ok
	}()
	<-started
	if err := container.Close(); err != nil {
		t.Fatal(err)
	}
	close(release)
	if <-resolved {
		t.Fatal("Get() succeeded after concurrent Close()")
	}
	if !service.closed {
		t.Fatal("late lazy resource was not closed")
	}
}
S
struct

Config

app/di/container_test.go:742-744
type Config struct

Fields

Name Type Description
DSN string
S
struct

UserService

app/di/container_test.go:746-749
type UserService struct

Fields

Name Type Description
DB *testDB
Cfg *Config
F
function

NewUserService

Parameters

db
cfg

Returns

app/di/container_test.go:751-753
func NewUserService(db *testDB, cfg *Config) UserService

{
	return UserService{DB: db, Cfg: cfg}
}
F
function

NewFailingUserService

Parameters

db
cfg

Returns

error
app/di/container_test.go:755-757
func NewFailingUserService(db *testDB, cfg *Config) (UserService, error)

{
	return UserService{}, errors.New("constructor failed")
}
F
function

TestRegisterFromFunc

Parameters

app/di/container_test.go:759-773
func TestRegisterFromFunc(t *testing.T)

{
	b := NewBuilder()
	Register(b, func() *testDB { return &testDB{Name: "pg"} })
	Register(b, func() *Config { return &Config{DSN: "host=localhost"} })
	RegisterFromFunc[UserService](b, NewUserService, Scoped)

	c := b.MustBuild()
	svc := ResolveType[UserService](c)
	if svc.DB.Name != "pg" {
		t.Errorf("DB.Name = %q, want %q", svc.DB.Name, "pg")
	}
	if svc.Cfg.DSN != "host=localhost" {
		t.Errorf("Cfg.DSN = %q, want %q", svc.Cfg.DSN, "host=localhost")
	}
}
F
function

TestRegisterFromFunc_MissingDep

Parameters

app/di/container_test.go:775-784
func TestRegisterFromFunc_MissingDep(t *testing.T)

{
	b := NewBuilder()
	Register(b, func() *testDB { return &testDB{Name: "pg"} })
	RegisterFromFunc[UserService](b, NewUserService, Scoped)

	_, err := b.Build()
	if err == nil {
		t.Fatal("expected build error for missing Config dependency")
	}
}
F
function

TestRegisterFromFunc_ReturnsConstructorError

Parameters

app/di/container_test.go:786-796
func TestRegisterFromFunc_ReturnsConstructorError(t *testing.T)

{
	b := NewBuilder()
	Register(b, func() *testDB { return &testDB{Name: "pg"} })
	Register(b, func() *Config { return &Config{DSN: "host=localhost"} })
	RegisterFromFunc[UserService](b, NewFailingUserService)

	c := b.MustBuild()
	if _, err := TryResolveType[UserService](c); err == nil {
		t.Fatal("TryResolveType() ignored the constructor error")
	}
}
F
function

TestRegisterFromFunc_ZeroArgumentConstructor

Parameters

app/di/container_test.go:798-806
func TestRegisterFromFunc_ZeroArgumentConstructor(t *testing.T)

{
	builder := NewBuilder()
	RegisterFromFunc[*testDB](builder, func() *testDB {
		return &testDB{Name: "zero"}
	})
	if got := ResolveType[*testDB](builder.MustBuild()).Name; got != "zero" {
		t.Fatalf("resolved name = %q, want zero", got)
	}
}
I
interface

failingInitializer

app/di/container_test.go:808-810
type failingInitializer interface

Methods

Ready
Method
func Ready(...)
S
struct
Implements: failingInitializer

initFailure

app/di/container_test.go:812-812
type initFailure struct

Methods

Ready
Method
func (*initFailure) Ready()
{}
Init
Method

Returns

error
func (*initFailure) Init() error
{
	return errors.New("init failed")
}
F
function

TestRegisterImpl_PropagatesInitError

Parameters

app/di/container_test.go:820-827
func TestRegisterImpl_PropagatesInitError(t *testing.T)

{
	builder := NewBuilder()
	RegisterImpl[failingInitializer, *initFailure](builder)
	container := builder.MustBuild()
	if _, err := TryResolveType[failingInitializer](container); err == nil {
		t.Fatal("TryResolveType() ignored Init error")
	}
}
S
struct

cyclicA

app/di/container_test.go:829-829
type cyclicA struct
S
struct

cyclicB

app/di/container_test.go:830-830
type cyclicB struct
F
function

newCyclicA

Parameters

Returns

app/di/container_test.go:832-834
func newCyclicA(*cyclicB) *cyclicA

{
	return &cyclicA{}
}
F
function

newCyclicB

Parameters

Returns

app/di/container_test.go:836-838
func newCyclicB(*cyclicA) *cyclicB

{
	return &cyclicB{}
}
F
function

TestRegisterFromFunc_RejectsCircularDependency

Parameters

app/di/container_test.go:840-848
func TestRegisterFromFunc_RejectsCircularDependency(t *testing.T)

{
	b := NewBuilder()
	RegisterFromFunc[*cyclicA](b, newCyclicA)
	RegisterFromFunc[*cyclicB](b, newCyclicB)

	if _, err := b.Build(); err == nil {
		t.Fatal("Build() accepted a circular constructor dependency")
	}
}
S
struct

serviceWithDB

app/di/container_test.go:850-852
type serviceWithDB struct

Fields

Name Type Description
DB *testDB
F
function

TestRegisterFromFunc_UsesRegisteredSingleton

Parameters

app/di/container_test.go:854-874
func TestRegisterFromFunc_UsesRegisteredSingleton(t *testing.T)

{
	b := NewBuilder()
	created := 0
	Register(b, func() *testDB {
		created++
		return &testDB{Name: "singleton"}
	})
	RegisterFromFunc[*serviceWithDB](b, func(db *testDB) *serviceWithDB {
		return &serviceWithDB{DB: db}
	})

	container := b.MustBuild()
	db := ResolveType[*testDB](container)
	service := ResolveType[*serviceWithDB](container)
	if service.DB != db {
		t.Fatal("constructor dependency did not use the registered singleton")
	}
	if created != 1 {
		t.Fatalf("singleton factory called %d times", created)
	}
}
S
struct

closableService

app/di/container_test.go:876-880
type closableService struct

Methods

Close
Method

Returns

error
func (*closableService) Close() error
{
	c.closed = true
	if c.order != nil {
		*c.order = append(*c.order, c.name)
	}
	return nil
}

Fields

Name Type Description
closed bool
order *[]string
name string
F
function

TestScopedContainer_Close

Parameters

app/di/container_test.go:890-906
func TestScopedContainer_Close(t *testing.T)

{
	b := NewBuilder()
	Register(b, func() *closableService { return &closableService{} }, Scoped)

	c := b.MustBuild()
	scope := c.Scope()
	svc := ResolveType[*closableService](scope)
	if svc.closed {
		t.Error("service should not be closed yet")
	}
	if err := scope.Close(); err != nil {
		t.Fatalf("Close: %v", err)
	}
	if !svc.closed {
		t.Error("service should be closed after scope.Close()")
	}
}
F
function

TestContainer_Close_Singleton

Parameters

app/di/container_test.go:908-923
func TestContainer_Close_Singleton(t *testing.T)

{
	b := NewBuilder()
	Register(b, func() *closableService { return &closableService{} }, Singleton)

	c := b.MustBuild()
	svc := ResolveType[*closableService](c)
	if svc.closed {
		t.Error("service should not be closed yet")
	}
	if err := c.Close(); err != nil {
		t.Fatalf("Close: %v", err)
	}
	if !svc.closed {
		t.Error("singleton implementing io.Closer should be closed on container close")
	}
}
F
function

TestContainer_CloseRegisteredInstance

Parameters

app/di/container_test.go:925-937
func TestContainer_CloseRegisteredInstance(t *testing.T)

{
	service := &closableService{}
	b := NewBuilder()
	RegisterInstance(b, service)
	container := b.MustBuild()

	if err := container.Close(); err != nil {
		t.Fatal(err)
	}
	if !service.closed {
		t.Fatal("Close() did not close a registered instance")
	}
}
F
function

TestContainer_Close_NonCloser

Parameters

app/di/container_test.go:939-948
func TestContainer_Close_NonCloser(t *testing.T)

{
	b := NewBuilder()
	Register(b, func() *testDB { return &testDB{Name: "ok"} })

	c := b.MustBuild()
	ResolveType[*testDB](c)
	if err := c.Close(); err != nil {
		t.Fatalf("Close on non-closer should not error: %v", err)
	}
}
S
struct

dependentCloser

app/di/container_test.go:950-953
type dependentCloser struct

Methods

Close
Method

Returns

error
func (*dependentCloser) Close() error
{
	*c.order = append(*c.order, "dependent")
	return nil
}

Fields

Name Type Description
service *closableService
order *[]string
F
function

TestContainer_CloseUsesReverseCreationOrder

Parameters

app/di/container_test.go:960-978
func TestContainer_CloseUsesReverseCreationOrder(t *testing.T)

{
	var order []string
	b := NewBuilder()
	Register(b, func() *closableService {
		return &closableService{name: "dependency", order: &order}
	})
	RegisterFromFunc[*dependentCloser](b, func(service *closableService) *dependentCloser {
		return &dependentCloser{service: service, order: &order}
	})

	container := b.MustBuild()
	ResolveType[*dependentCloser](container)
	if err := container.Close(); err != nil {
		t.Fatal(err)
	}
	if len(order) != 2 || order[0] != "dependent" || order[1] != "dependency" {
		t.Fatalf("close order = %v", order)
	}
}
S
struct

Key

Key is a typed name for a dependency.

app/di/key.go:6-8
type Key struct

Fields

Name Type Description
name string
F
function

NewKey

NewKey creates a typed dependency key.

Parameters

name
string

Returns

Key[T]
app/di/key.go:11-16
func NewKey[T any](name string) Key[T]

{
	if name == "" {
		panic("di: dependency name cannot be empty")
	}
	return Key[T]{name: name}
}
F
function

ProvideKey

ProvideKey registers a typed named dependency.

Parameters

b
key
Key[T]
instance
T
app/di/key.go:24-26
func ProvideKey[T any](b *Builder, key Key[T], instance T)

{
	b.Provide(key.name, instance)
}
F
function

ResolveKey

ResolveKey retrieves a dependency through a typed key.

Parameters

key
Key[T]

Returns

T
bool
app/di/key.go:29-31
func ResolveKey[T any](c *Container, key Key[T]) (T, bool)

{
	return Resolve[T](c, key.name)
}
F
function

MustResolveKey

MustResolveKey retrieves a dependency through a typed key and panics when missing.

Parameters

key
Key[T]

Returns

T
app/di/key.go:34-36
func MustResolveKey[T any](c *Container, key Key[T]) T

{
	return MustResolve[T](c, key.name)
}
F
function

ProvideLazyKey

ProvideLazyKey registers a typed lazy dependency.

Parameters

key
Key[T]
factory
func() T
app/di/key.go:39-46
func ProvideLazyKey[T any](c *Container, key Key[T], factory func() T)

{
	if factory == nil {
		panic("di: lazy dependency factory cannot be nil")
	}
	c.ProvideLazy(key.name, func() any {
		return factory()
	}, reflect.TypeOf((*T)(nil)).Elem())
}
F
function

TestKey

Parameters

app/di/key_test.go:5-15
func TestKey(t *testing.T)

{
	key := NewKey[int]("answer")
	b := NewBuilder()
	ProvideKey(b, key, 42)

	c := b.MustBuild()
	got, ok := ResolveKey(c, key)
	if !ok || got != 42 {
		t.Fatalf("ResolveKey() = %v, %v", got, ok)
	}
}
F
function

TestKeyRejectsWrongType

Parameters

app/di/key_test.go:17-25
func TestKeyRejectsWrongType(t *testing.T)

{
	key := NewKey[int]("answer")
	c := New()
	c.Provide(key.Name(), "wrong")

	if _, ok := ResolveKey(c, key); ok {
		t.Fatal("ResolveKey() accepted the wrong type")
	}
}
F
function

TestProvideLazyKeyRejectsNilFactory

Parameters

app/di/key_test.go:27-34
func TestProvideLazyKeyRejectsNilFactory(t *testing.T)

{
	defer func() {
		if recover() == nil {
			t.Fatal("ProvideLazyKey() did not panic")
		}
	}()
	ProvideLazyKey(New(), NewKey[int]("answer"), nil)
}
F
function

TestProvideLazyKeyCanBeValidatedWithoutInstantiation

Parameters

app/di/key_test.go:36-52
func TestProvideLazyKeyCanBeValidatedWithoutInstantiation(t *testing.T)

{
	type target struct {
		Answer int `inject:"answer"`
	}
	container := New()
	called := false
	ProvideLazyKey(container, NewKey[int]("answer"), func() int {
		called = true
		return 42
	})
	if err := container.ValidateTarget(&target{}); err != nil {
		t.Fatal(err)
	}
	if called {
		t.Fatal("ValidateTarget() instantiated a lazy dependency")
	}
}
F
function

Resolve

Resolve retrieves a typed dependency from the container by name.

Parameters

name
string

Returns

T
bool
app/di/resolve.go:4-15
func Resolve[T any](c *Container, name string) (T, bool)

{
	var zero T
	v, ok := c.Get(name)
	if !ok {
		return zero, false
	}
	typed, ok := v.(T)
	if !ok {
		return zero, false
	}
	return typed, true
}
F
function

MustResolve

MustResolve retrieves a typed dependency by name and panics if not found.

Parameters

name
string

Returns

T
app/di/resolve.go:18-24
func MustResolve[T any](c *Container, name string) T

{
	v, ok := Resolve[T](c, name)
	if !ok {
		panic("di: cannot resolve " + name)
	}
	return v
}
F
function

ResolveAll

ResolveAll finds all named dependencies that implement the given interface T.

Parameters

Returns

[]T
app/di/resolve.go:27-55
func ResolveAll[T any](c *Container) []T

{
	if c == nil || c.ensureOpen() != nil {
		return nil
	}
	c.mu.RLock()
	values := make([]any, 0, len(c.named))
	for _, value := range c.named {
		values = append(values, value)
	}
	c.mu.RUnlock()

	var result []T
	for _, v := range values {
		if lp, ok := v.(*lazyProvider); ok {
			var err error
			v, err = lp.get()
			if err != nil {
				continue
			}
			if lp.owner.trackCloser(v) != nil {
				continue
			}
		}
		if t, ok := v.(T); ok {
			result = append(result, t)
		}
	}
	return result
}
T
type

Lifetime

Lifetime controls how services are instantiated.

app/di/container.go:15-15
type Lifetime int
S
struct

serviceEntry

app/di/container.go:26-35
type serviceEntry struct

Fields

Name Type Description
lifetime Lifetime
factory any
constructor any
instance any
built bool
buildMu sync.Mutex
concreteType reflect.Type
paramTypes []reflect.Type
S
struct

ResolveError

ResolveError is returned when a type cannot be resolved.

app/di/container.go:38-40
type ResolveError struct

Methods

Error
Method

Returns

string
func (*ResolveError) Error() string
{
	return fmt.Sprintf("di: cannot resolve type %v", e.Type)
}

Fields

Name Type Description
Type reflect.Type
S
struct

Container

Container is the dependency injection container.

app/di/container.go:47-55
type Container struct

Methods

validate
Method

Returns

[]error
func (*Container) validate() []error
{
	var errs []error
	states := make(map[reflect.Type]uint8)

	for typ := range c.services {
		if err := c.validateType(typ, states); err != nil {
			errs = append(errs, err)
		}
	}
	return errs
}
validateType
Method

Parameters

states map[reflect.Type]uint8

Returns

error
func (*Container) validateType(typ reflect.Type, states map[reflect.Type]uint8) error
{
	switch states[typ] {
	case 1:
		return fmt.Errorf("di: circular dependency detected for type %v", typ)
	case 2:
		return nil
	}
	states[typ] = 1
	defer func() {
		if states[typ] == 1 {
			delete(states, typ)
		}
	}()

	entry, ok := c.services[typ]
	if !ok {
		return &ResolveError{Type: typ}
	}

	if entry.constructor != nil {
		for _, pt := range entry.paramTypes {
			if err := c.validateType(pt, states); err != nil {
				return fmt.Errorf("di: type %v requires %v: %w", typ, pt, err)
			}
		}
	}

	if entry.factory == nil && entry.constructor == nil {
		if err := c.validateConcreteType(entry.concreteType, states); err != nil {
			return err
		}
	}

	states[typ] = 2
	return nil
}

Parameters

states map[reflect.Type]uint8

Returns

error
func (*Container) validateConcreteType(typ reflect.Type, states map[reflect.Type]uint8) error
{
	if typ.Kind() == reflect.Ptr {
		typ = typ.Elem()
	}
	if typ.Kind() != reflect.Struct {
		return fmt.Errorf("di: auto-construction requires struct type, got %v", typ)
	}

	if initMethod, ok := reflect.PointerTo(typ).MethodByName("Init"); ok {
		errorType := reflect.TypeOf((*error)(nil)).Elem()
		if initMethod.Type.NumOut() > 1 ||
			initMethod.Type.NumOut() == 1 && initMethod.Type.Out(0) != errorType {
			return fmt.Errorf("di: type %v Init must return nothing or error", typ)
		}
		for i := 1; i < initMethod.Type.NumIn(); i++ {
			paramType := initMethod.Type.In(i)
			if err := c.validateType(paramType, states); err != nil {
				return fmt.Errorf("di: type %v Init requires %v: %w", typ, paramType, err)
			}
		}
		return nil
	}

	for i := 0; i < typ.NumField(); i++ {
		field := typ.Field(i)
		tag := field.Tag.Get("inject")
		if tag == "" {
			continue
		}
		if !field.IsExported() {
			return fmt.Errorf("di: struct %v injected field %s must be exported", typ, field.Name)
		}
		if err := c.validateType(field.Type, states); err != nil {
			return fmt.Errorf(
				"di: struct %v field %s requires %v: %w",
				typ,
				field.Name,
				field.Type,
				err,
			)
		}
	}
	return nil
}
trackCloser
Method

Parameters

instance any

Returns

error
func (*Container) trackCloser(instance any) error
{
	if closer, ok := instance.(io.Closer); ok {
		c.closeMu.Lock()
		if c.closed {
			c.closeMu.Unlock()
			_ = closer.Close()
			return ErrContainerClosed
		}
		c.trackCloserLocked(closer)
		c.closeMu.Unlock()
	}
	return nil
}

Parameters

closer io.Closer
func (*Container) trackCloserLocked(closer io.Closer)
{
	for _, existing := range c.closers {
		if sameCloser(existing, closer) {
			return
		}
	}
	c.closers = append(c.closers, closer)
}
ensureOpen
Method

Returns

error
func (*Container) ensureOpen() error
{
	c.closeMu.Lock()
	defer c.closeMu.Unlock()
	if c.closed {
		return ErrContainerClosed
	}
	return nil
}
Close
Method

Close calls Close() on all resolved services that implement io.Closer. Call this at the end of a scope (e.g. at end of HTTP request).

Returns

error
func (*Container) Close() error
{
	c.closeMu.Lock()
	if c.closed {
		c.closeMu.Unlock()
		return nil
	}
	c.closed = true
	closers := make([]io.Closer, len(c.closers))
	copy(closers, c.closers)
	c.closers = nil
	c.closeMu.Unlock()

	var errs []error
	for index := len(closers) - 1; index >= 0; index-- {
		if err := closers[index].Close(); err != nil {
			errs = append(errs, err)
		}
	}
	if len(errs) > 0 {
		return fmt.Errorf("di: close errors: %v", errs)
	}
	return nil
}
Provide
Method

Parameters

name string
instance any
func (*Container) Provide(name string, instance any)
{
	if isNilInstance(instance) {
		panic(fmt.Sprintf("di: dependency %q cannot be nil", name))
	}
	contracts.MustVerify(instance)
	if name == "" {
		panic("di: dependency name cannot be empty")
	}
	c.closeMu.Lock()
	if c.closed {
		c.closeMu.Unlock()
		panic(ErrContainerClosed)
	}
	c.mu.Lock()
	if _, exists := c.named[name]; exists {
		c.mu.Unlock()
		c.closeMu.Unlock()
		panic(fmt.Sprintf("di: dependency %q is already registered", name))
	}
	c.named[name] = instance
	c.mu.Unlock()
	if closer, ok := instance.(io.Closer); ok {
		c.trackCloserLocked(closer)
	}
	c.closeMu.Unlock()
}
Get
Method

Parameters

name string

Returns

any
bool
func (*Container) Get(name string) (any, bool)
{
	if c.ensureOpen() != nil {
		return nil, false
	}
	c.mu.RLock()
	v, ok := c.named[name]
	c.mu.RUnlock()
	if !ok {
		return nil, false
	}
	if lp, ok := v.(*lazyProvider); ok {
		value, err := lp.get()
		if err != nil {
			return nil, false
		}
		if lp.owner.trackCloser(value) != nil {
			return nil, false
		}
		return value, true
	}
	return v, true
}
MustGet
Method

Parameters

name string

Returns

any
func (*Container) MustGet(name string) any
{
	v, ok := c.Get(name)
	if !ok {
		panic("di: dependency not found: " + name)
	}
	return v
}
Has
Method

Parameters

name string

Returns

bool
func (*Container) Has(name string) bool
{
	if c.ensureOpen() != nil {
		return false
	}
	c.mu.RLock()
	_, ok := c.named[name]
	c.mu.RUnlock()
	return ok
}

ValidateTarget checks named injection fields without changing target.

Parameters

target any

Returns

error
func (*Container) ValidateTarget(target any) error
{
	if err := c.ensureOpen(); err != nil {
		return err
	}
	val := reflect.ValueOf(target)
	if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Struct {
		return fmt.Errorf("di: injection target must be a pointer to a struct")
	}
	fields := injectParser.ParseStruct(target)

	for _, meta := range fields {
		field := val.Elem().Field(meta.Index)
		if !field.CanSet() {
			return fmt.Errorf("di: injected field %s must be exported and settable", meta.Name)
		}
		name := meta.RawTag
		if name == "" {
			name = meta.Name
		}
		c.mu.RLock()
		dep, ok := c.named[name]
		c.mu.RUnlock()
		if !ok {
			return fmt.Errorf("di: dependency %q required by field %s is not registered", name, meta.Name)
		}
		depType := reflect.TypeOf(dep)
		if lazy, isLazy := dep.(*lazyProvider); isLazy {
			depType = lazy.resultType
			if depType == nil {
				return fmt.Errorf(
					"di: lazy dependency %q has no type metadata; use a typed key or provide its result type",
					name,
				)
			}
		}
		if depType == nil || !depType.AssignableTo(field.Type()) {
			return fmt.Errorf("di: dependency %q has type %v, field %s requires %v", name, depType, meta.Name, field.Type())
		}
	}
	return nil
}
Inject
Method

Inject validates and populates named injection fields.

Parameters

target any

Returns

error
func (*Container) Inject(target any) error
{
	if err := c.ValidateTarget(target); err != nil {
		return err
	}

	elem := reflect.ValueOf(target).Elem()
	fields := injectParser.ParseStruct(target)

	for _, meta := range fields {
		field := elem.Field(meta.Index)
		if !field.CanSet() {
			return fmt.Errorf("di: injected field %s must be exported and settable", meta.Name)
		}
		name := meta.RawTag
		if name == "" {
			name = meta.Name
		}
		dep, _ := c.Get(name)
		field.Set(reflect.ValueOf(dep))
	}
	return nil
}
Scope
Method

Scope creates a child container with fresh scoped instances. When the scope ends, call Close() to dispose scoped services.

Returns

func (*Container) Scope() *Container
{
	if err := c.ensureOpen(); err != nil {
		panic(err)
	}
	c.mu.RLock()
	defer c.mu.RUnlock()

	child := New()
	child.parent = c
	for k, v := range c.services {
		if v.lifetime == Scoped {
			child.services[k] = &serviceEntry{
				lifetime:     v.lifetime,
				factory:      v.factory,
				concreteType: v.concreteType,
				constructor:  v.constructor,
				paramTypes:   v.paramTypes,
			}
		} else {
			child.services[k] = v
		}
	}
	for k, v := range c.named {
		child.named[k] = v
	}
	return child
}
ProvideLazy
Method

Parameters

name string
factory func() any
resultType ...reflect.Type
func (*Container) ProvideLazy(name string, factory func() any, resultType ...reflect.Type)
{
	if name == "" {
		panic("di: dependency name cannot be empty")
	}
	if factory == nil {
		panic("di: lazy dependency factory cannot be nil")
	}
	c.closeMu.Lock()
	if c.closed {
		c.closeMu.Unlock()
		panic(ErrContainerClosed)
	}
	c.mu.Lock()
	if _, exists := c.named[name]; exists {
		c.mu.Unlock()
		c.closeMu.Unlock()
		panic(fmt.Sprintf("di: dependency %q is already registered", name))
	}
	var typ reflect.Type
	if len(resultType) > 0 {
		typ = resultType[0]
	}
	c.named[name] = &lazyProvider{factory: factory, owner: c, resultType: typ}
	c.mu.Unlock()
	c.closeMu.Unlock()
}
Clone
Method

Returns

func (*Container) Clone() *Container
{
	if err := c.ensureOpen(); err != nil {
		panic(err)
	}
	c.mu.RLock()
	defer c.mu.RUnlock()

	clone := New()
	clone.parent = c
	for k, v := range c.services {
		clone.services[k] = v
	}
	for k, v := range c.named {
		clone.named[k] = v
	}
	return clone
}
Keys
Method

Returns

[]string
func (*Container) Keys() []string
{
	if c.ensureOpen() != nil {
		return nil
	}
	c.mu.RLock()
	defer c.mu.RUnlock()

	keys := make([]string, 0, len(c.named))
	for k := range c.named {
		keys = append(keys, k)
	}
	return keys
}

Parameters

iface reflect.Type

Returns

[]any
func (*Container) ResolveAllTyped(iface reflect.Type) []any
{
	if c.ensureOpen() != nil {
		return nil
	}
	c.mu.RLock()
	values := make([]any, 0, len(c.named))
	for _, value := range c.named {
		values = append(values, value)
	}
	c.mu.RUnlock()

	var result []any
	for _, v := range values {
		if lp, ok := v.(*lazyProvider); ok {
			var err error
			v, err = lp.get()
			if err != nil {
				continue
			}
			if lp.owner.trackCloser(v) != nil {
				continue
			}
		}
		if !isNilInstance(v) && reflect.TypeOf(v).Implements(iface) {
			result = append(result, v)
		}
	}
	return result
}

Fields

Name Type Description
services map[reflect.Type]*serviceEntry
named map[string]any
mu sync.RWMutex
parent *Container
closers []io.Closer
closeMu sync.Mutex
closed bool
F
function

New

New creates a new empty Container.

Returns

app/di/container.go:62-67
func New() *Container

{
	return &Container{
		services: make(map[reflect.Type]*serviceEntry),
		named:    make(map[string]any),
	}
}
S
struct

Builder

Builder constructs a Container with validation.

app/di/container.go:70-76
type Builder struct

Methods

Build
Method

Build creates a Container and validates all registrations. Returns an error if any registered type has unresolvable dependencies.

Returns

error
func (*Builder) Build() (*Container, error)
{
	c := New()
	b.mu.RLock()
	buildErrors := append([]error(nil), b.buildErrors...)
	for k, v := range b.services {
		c.services[k] = cloneServiceEntry(v)
	}
	for k, v := range b.named {
		c.named[k] = v
	}
	b.mu.RUnlock()

	if len(buildErrors) > 0 {
		return c, fmt.Errorf("di: build validation failed: %v", buildErrors)
	}
	if errs := c.validate(); len(errs) > 0 {
		return c, fmt.Errorf("di: build validation failed: %v", errs)
	}
	for _, entry := range c.services {
		if entry.built {
			_ = c.trackCloser(entry.instance)
		}
	}
	for _, instance := range c.named {
		if _, lazy := instance.(*lazyProvider); !lazy {
			_ = c.trackCloser(instance)
		}
	}
	return c, nil
}
MustBuild
Method

MustBuild is like Build but panics on validation errors.

Returns

func (*Builder) MustBuild() *Container
{
	c, err := b.Build()
	if err != nil {
		panic(err.Error())
	}
	return c
}
Provide
Method

Parameters

name string
instance any
func (*Builder) Provide(name string, instance any)
{
	if isNilInstance(instance) {
		b.mu.Lock()
		b.buildErrors = append(b.buildErrors, fmt.Errorf("di: dependency %q cannot be nil", name))
		b.mu.Unlock()
		return
	}
	contracts.MustVerify(instance)
	b.mu.Lock()
	if name == "" {
		b.buildErrors = append(b.buildErrors, fmt.Errorf("di: dependency name cannot be empty"))
		b.mu.Unlock()
		return
	}
	if _, exists := b.named[name]; exists {
		b.buildErrors = append(b.buildErrors, fmt.Errorf("di: dependency %q is already registered", name))
		b.mu.Unlock()
		return
	}
	b.named[name] = instance
	b.mu.Unlock()
}

Fields

Name Type Description
services map[reflect.Type]*serviceEntry
named map[string]any
mu sync.RWMutex
validated bool
buildErrors []error
F
function

NewBuilder

NewBuilder creates a new Builder.

Returns

app/di/container.go:79-84
func NewBuilder() *Builder

{
	return &Builder{
		services: make(map[reflect.Type]*serviceEntry),
		named:    make(map[string]any),
	}
}
F
function

Register

Register adds a service factory to the builder.

Parameters

b
factory
func() T
lifetime
...Lifetime
app/di/container.go:87-106
func Register[T any](b *Builder, factory func() T, lifetime ...Lifetime)

{
	lt := Singleton
	if len(lifetime) > 0 {
		lt = lifetime[0]
	}
	typ := reflect.TypeOf((*T)(nil)).Elem()
	b.mu.Lock()
	if factory == nil {
		b.buildErrors = append(b.buildErrors, fmt.Errorf("di: factory for %v cannot be nil", typ))
		b.mu.Unlock()
		return
	}
	if _, exists := b.services[typ]; exists {
		b.buildErrors = append(b.buildErrors, fmt.Errorf("di: type %v is already registered", typ))
		b.mu.Unlock()
		return
	}
	b.services[typ] = &serviceEntry{lifetime: lt, factory: factory, concreteType: typ}
	b.mu.Unlock()
}
F
function

RegisterAs

RegisterAs registers a service factory resolving to a different concrete type.

Parameters

b
factory
func() T
lifetime
...Lifetime
app/di/container.go:109-134
func RegisterAs[I, T any](b *Builder, factory func() T, lifetime ...Lifetime)

{
	lt := Singleton
	if len(lifetime) > 0 {
		lt = lifetime[0]
	}
	typ := reflect.TypeOf((*I)(nil)).Elem()
	concreteType := reflect.TypeOf((*T)(nil)).Elem()
	b.mu.Lock()
	if factory == nil {
		b.buildErrors = append(b.buildErrors, fmt.Errorf("di: factory for %v cannot be nil", typ))
		b.mu.Unlock()
		return
	}
	if !concreteType.Implements(typ) {
		b.buildErrors = append(b.buildErrors, fmt.Errorf("di: type %v does not implement %v", concreteType, typ))
		b.mu.Unlock()
		return
	}
	if _, exists := b.services[typ]; exists {
		b.buildErrors = append(b.buildErrors, fmt.Errorf("di: type %v is already registered", typ))
		b.mu.Unlock()
		return
	}
	b.services[typ] = &serviceEntry{lifetime: lt, factory: factory, concreteType: concreteType}
	b.mu.Unlock()
}
F
function

RegisterInstance

RegisterInstance registers a pre-created instance as a singleton.

Parameters

b
instance
T
app/di/container.go:137-158
func RegisterInstance[T any](b *Builder, instance T)

{
	typ := reflect.TypeOf((*T)(nil)).Elem()
	b.mu.Lock()
	if isNilInstance(instance) {
		b.buildErrors = append(b.buildErrors, fmt.Errorf("di: instance for %v cannot be nil", typ))
		b.mu.Unlock()
		return
	}
	if _, exists := b.services[typ]; exists {
		b.buildErrors = append(b.buildErrors, fmt.Errorf("di: type %v is already registered", typ))
		b.mu.Unlock()
		return
	}
	b.services[typ] = &serviceEntry{
		lifetime:     Singleton,
		concreteType: typ,
		factory:      func() T { return instance },
		built:        true,
		instance:     instance,
	}
	b.mu.Unlock()
}
F
function

RegisterImpl

RegisterImpl registers a concrete type T that satisfies interface I.

Parameters

b
lifetime
...Lifetime
app/di/container.go:161-181
func RegisterImpl[I, T any](b *Builder, lifetime ...Lifetime)

{
	lt := Singleton
	if len(lifetime) > 0 {
		lt = lifetime[0]
	}
	iTyp := reflect.TypeOf((*I)(nil)).Elem()
	tTyp := reflect.TypeOf((*T)(nil)).Elem()
	b.mu.Lock()
	if !tTyp.Implements(iTyp) {
		b.buildErrors = append(b.buildErrors, fmt.Errorf("di: type %v does not implement %v", tTyp, iTyp))
		b.mu.Unlock()
		return
	}
	if _, exists := b.services[iTyp]; exists {
		b.buildErrors = append(b.buildErrors, fmt.Errorf("di: type %v is already registered", iTyp))
		b.mu.Unlock()
		return
	}
	b.services[iTyp] = &serviceEntry{lifetime: lt, factory: nil, concreteType: tTyp}
	b.mu.Unlock()
}
F
function

RegisterFromFunc

RegisterFromFunc registers a service by constructor function with dependency injection.

Parameters

b
constructor
any
lifetime
...Lifetime
app/di/container.go:184-222
func RegisterFromFunc[T any](b *Builder, constructor any, lifetime ...Lifetime)

{
	lt := Singleton
	if len(lifetime) > 0 {
		lt = lifetime[0]
	}
	typ := reflect.TypeOf((*T)(nil)).Elem()

	ctorType := reflect.TypeOf(constructor)
	if ctorType == nil || ctorType.Kind() != reflect.Func {
		panic("di: RegisterFromFunc requires a function")
	}
	if ctorType.NumOut() < 1 || ctorType.NumOut() > 2 {
		panic("di: RegisterFromFunc constructor must return a value and optional error")
	}
	if !ctorType.Out(0).AssignableTo(typ) {
		panic(fmt.Sprintf("di: RegisterFromFunc constructor returns %v, want %v", ctorType.Out(0), typ))
	}
	errorType := reflect.TypeOf((*error)(nil)).Elem()
	if ctorType.NumOut() == 2 && ctorType.Out(1) != errorType {
		panic("di: RegisterFromFunc second result must be error")
	}

	paramTypes := make([]reflect.Type, ctorType.NumIn())
	for i := 0; i < ctorType.NumIn(); i++ {
		paramTypes[i] = ctorType.In(i)
	}

	b.mu.Lock()
	if _, exists := b.services[typ]; exists {
		b.buildErrors = append(b.buildErrors, fmt.Errorf("di: type %v is already registered", typ))
		b.mu.Unlock()
		return
	}
	b.services[typ] = &serviceEntry{
		lifetime: lt, factory: nil, concreteType: typ,
		constructor: constructor, paramTypes: paramTypes,
	}
	b.mu.Unlock()
}
F
function

cloneServiceEntry

Parameters

entry

Returns

app/di/container.go:257-267
func cloneServiceEntry(entry *serviceEntry) *serviceEntry

{
	return &serviceEntry{
		lifetime:     entry.lifetime,
		factory:      entry.factory,
		constructor:  entry.constructor,
		instance:     entry.instance,
		built:        entry.built,
		concreteType: entry.concreteType,
		paramTypes:   append([]reflect.Type(nil), entry.paramTypes...),
	}
}
F
function

ResolveType

ResolveType resolves a service from the container, panicking on failure.

Parameters

Returns

T
app/di/container.go:396-402
func ResolveType[T any](c *Container) T

{
	result, err := tryResolveType(c, reflect.TypeOf((*T)(nil)).Elem())
	if err != nil {
		panic(err.Error())
	}
	return result.Interface().(T)
}
F
function

TryResolveType

TryResolveType resolves a service from the container, returning an error on failure.

Parameters

Returns

T
error
app/di/container.go:405-412
func TryResolveType[T any](c *Container) (T, error)

{
	var zero T
	result, err := tryResolveType(c, reflect.TypeOf((*T)(nil)).Elem())
	if err != nil {
		return zero, err
	}
	return result.Interface().(T), nil
}
F
function

tryResolveType

Parameters

Returns

app/di/container.go:414-416
func tryResolveType(c *Container, typ reflect.Type) (reflect.Value, error)

{
	return resolveByType(c, typ, make(map[reflect.Type]struct{}))
}
F
function

resolveByType

Parameters

stack
map[reflect.Type]struct{}

Returns

app/di/container.go:418-478
func resolveByType(c *Container, typ reflect.Type, stack map[reflect.Type]struct{}) (reflect.Value, error)

{
	if err := c.ensureOpen(); err != nil {
		return reflect.Value{}, err
	}
	c.mu.RLock()
	entry, ok := c.services[typ]
	c.mu.RUnlock()

	if !ok {
		if c.parent != nil {
			return resolveByType(c.parent, typ, stack)
		}
		return reflect.Value{}, &ResolveError{Type: typ}
	}
	if entry.lifetime == Singleton && c.parent != nil {
		return resolveByType(c.parent, typ, stack)
	}
	if _, resolving := stack[typ]; resolving {
		return reflect.Value{}, fmt.Errorf("di: circular dependency detected for type %v", typ)
	}
	stack[typ] = struct{}{}
	defer func() {
		delete(stack, typ)
	}()

	switch entry.lifetime {
	case Singleton, Scoped:
		entry.buildMu.Lock()
		defer entry.buildMu.Unlock()
		if entry.built {
			return reflect.ValueOf(entry.instance), nil
		}
		result, err := invokeEntry(c, entry, stack)
		if err != nil {
			return reflect.Value{}, err
		}
		if isNilReflectValue(result) {
			return reflect.Value{}, fmt.Errorf("di: factory for %v returned nil", typ)
		}
		entry.instance = result.Interface()
		entry.built = true
		if err := c.trackCloser(entry.instance); err != nil {
			return reflect.Value{}, err
		}
		return result, nil
	case Transient:
		result, err := invokeEntry(c, entry, stack)
		if err != nil {
			return reflect.Value{}, err
		}
		if isNilReflectValue(result) {
			return reflect.Value{}, fmt.Errorf("di: factory for %v returned nil", typ)
		}
		if err := c.trackCloser(result.Interface()); err != nil {
			return reflect.Value{}, err
		}
		return result, nil
	default:
		return reflect.Value{}, &ResolveError{Type: typ}
	}
}
F
function

sameCloser

Parameters

left
right

Returns

bool
app/di/container.go:512-519
func sameCloser(left, right io.Closer) bool

{
	leftType := reflect.TypeOf(left)
	rightType := reflect.TypeOf(right)
	if leftType != rightType || leftType == nil || !leftType.Comparable() {
		return false
	}
	return left == right
}
F
function

invokeEntry

Parameters

entry
stack
map[reflect.Type]struct{}

Returns

app/di/container.go:547-556
func invokeEntry(c *Container, entry *serviceEntry, stack map[reflect.Type]struct{}) (reflect.Value, error)

{
	if entry.constructor != nil {
		return invokeConstructor(c, entry, stack)
	}
	if entry.factory != nil {
		results := reflect.ValueOf(entry.factory).Call(nil)
		return results[0], nil
	}
	return constructValue(c, entry.concreteType, stack)
}
F
function

invokeConstructor

Parameters

entry
stack
map[reflect.Type]struct{}

Returns

app/di/container.go:558-573
func invokeConstructor(c *Container, entry *serviceEntry, stack map[reflect.Type]struct{}) (reflect.Value, error)

{
	ctorVal := reflect.ValueOf(entry.constructor)
	args := make([]reflect.Value, len(entry.paramTypes))
	for i, pt := range entry.paramTypes {
		resolved, err := resolveByType(c, pt, stack)
		if err != nil {
			return reflect.Value{}, fmt.Errorf("di: cannot resolve param %d (%v) of constructor for %v: %w", i, pt, entry.concreteType, err)
		}
		args[i] = resolved
	}
	results := ctorVal.Call(args)
	if len(results) == 2 && !results[1].IsNil() {
		return reflect.Value{}, results[1].Interface().(error)
	}
	return results[0], nil
}
F
function

constructValue

Parameters

stack
map[reflect.Type]struct{}

Returns

app/di/container.go:575-621
func constructValue(c *Container, typ reflect.Type, stack map[reflect.Type]struct{}) (reflect.Value, error)

{
	if typ.Kind() == reflect.Ptr {
		typ = typ.Elem()
	}
	if typ.Kind() != reflect.Struct {
		return reflect.Value{}, fmt.Errorf("di: auto-construction requires struct type, got %v", typ)
	}

	val := reflect.New(typ)

	ctorVal := val.MethodByName("Init")
	if ctorVal.IsValid() {
		ctorType := ctorVal.Type()
		args := make([]reflect.Value, ctorType.NumIn())
		for i := 0; i < ctorType.NumIn(); i++ {
			resolved, err := resolveByType(c, ctorType.In(i), stack)
			if err != nil {
				return reflect.Value{}, fmt.Errorf("di: cannot resolve param %d of Init: %w", i, err)
			}
			args[i] = resolved
		}
		results := ctorVal.Call(args)
		if len(results) == 1 && !results[0].IsNil() {
			return reflect.Value{}, results[0].Interface().(error)
		}
	} else {
		elem := val.Elem()
		for i := 0; i < typ.NumField(); i++ {
			field := typ.Field(i)
			tag := field.Tag.Get("inject")
			if tag == "" {
				continue
			}
			fieldVal := elem.Field(i)
			if !fieldVal.CanSet() || !field.IsExported() {
				return reflect.Value{}, fmt.Errorf("di: injected field %s must be exported and settable", field.Name)
			}
			resolved, err := resolveByType(c, fieldVal.Type(), stack)
			if err != nil {
				return reflect.Value{}, fmt.Errorf("di: cannot resolve field %s: %w", field.Name, err)
			}
			fieldVal.Set(resolved)
		}
	}

	return val, nil
}
F
function

isNilInstance

Parameters

instance
any

Returns

bool
app/di/container.go:877-889
func isNilInstance(instance any) bool

{
	if instance == nil {
		return true
	}
	value := reflect.ValueOf(instance)
	switch value.Kind() {
	case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map,
		reflect.Ptr, reflect.Slice:
		return value.IsNil()
	default:
		return false
	}
}
F
function

isNilReflectValue

Parameters

Returns

bool
app/di/container.go:891-902
func isNilReflectValue(value reflect.Value) bool

{
	if !value.IsValid() {
		return true
	}
	switch value.Kind() {
	case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map,
		reflect.Ptr, reflect.Slice:
		return value.IsNil()
	default:
		return false
	}
}
S
struct

lazyProvider

app/di/container.go:904-911
type lazyProvider struct

Methods

get
Method

Returns

any
error
func (*lazyProvider) get() (any, error)
{
	l.once.Do(func() {
		l.value = l.factory()
		if isNilInstance(l.value) {
			l.err = errors.New("di: lazy dependency factory returned nil")
		}
	})
	return l.value, l.err
}

Fields

Name Type Description
once sync.Once
value any
err error
factory func() any
owner *Container
resultType reflect.Type