-
Notifications
You must be signed in to change notification settings - Fork 0
Home
A reflection-based dependency injection container for Go, in roughly 900 lines of implementation. It is the container behind rex, but it has no dependency on the framework and works standalone.
go get github.com/kryovyx/dixRequires Go 1.27+.
- Four registration kinds — Singleton, Scoped, Transient, Instance
-
Child scopes with automatic
Close()of everything scoped that has one - Resolution by concrete type or by interface
- Every failure is a sentinel error, so callers branch with
errors.Is - Safe for concurrent registration and resolution
No struct-tag injection, no constructor auto-wiring, no cycle detection, no named bindings. A factory asks the resolver for what it needs, in Go, and you can read the wiring by reading the code. See Limitations.
package main
import (
"fmt"
"github.com/kryovyx/dix"
)
type Database struct{ DSN string }
type UserRepo struct{ DB *Database }
func main() {
c := dix.New()
// Built once, on first resolve, shared forever.
_ = c.Singleton(func() *Database {
return &Database{DSN: "postgres://localhost/app"}
})
// Built once per scope. The Resolver it receives IS the scope.
_ = c.Scoped(func(r dix.Resolver) *UserRepo {
var db *Database
if err := r.Resolve(&db); err != nil {
panic(err)
}
return &UserRepo{DB: db}
})
scope := c.NewScope()
defer scope.Close()
var repo *UserRepo
if err := scope.Resolve(&repo); err != nil {
panic(err)
}
fmt.Println(repo.DB.DSN)
}Two things in that snippet are the whole design in miniature:
-
UserRepois resolved from the scope, not fromc. Resolving a scoped type from the root container returnsErrScopedFromRootand builds nothing — a scoped value with no owning scope is a value nothing will ever close. - The factory takes a
dix.Resolverand uses that, not the capturedc. Closing over the container would resolve*Databasefrom the root, which is fine for a singleton and wrong for anything scoped.
type Resolver interface {
Resolve(target any) error // *target = the dependency
ResolveAll(target any) error // append everything resolvable
}
type Container interface {
Resolver
Singleton(factory any) error
Scoped(factory any) error
Transient(factory any) error
Instance(v any) error
Unbind(v any) (bool, error)
NewScope() Scope
}
type Scope interface {
Resolver
Close() error
}Resolver is the one most code should ask for. A handler, a health check and a
factory all need to read dependencies; only wiring code needs to register.
Note for extension authors. Do not import
dixto name these types.rextension/dideclares the identical contract andrextensionre-exports it, so an extension writesrextension.Resolverand never names an implementation. See rextension → Dependency Injection.
| If you want to know | Read |
|---|---|
| Which lifetime to pick | Lifetimes |
| What a valid factory looks like | Registration |
| How interface resolution picks a match | Resolution |
| When things get closed | Scopes |
| What an error means | Errors |
| Whether you can register from a goroutine | Concurrency |
| Patterns for requests, tests, and cleanup | Recipes |
dix — Dependency Injection eXperience · MIT · © 2026 Kryovyx · pre-1.0, interfaces may change
Concepts
Reference
Practice
Ecosystem