Dependency Injection
go.putnami.dev/inject provides a hierarchical DI container with generics, named tokens, scoped providers, and cycle detection. DI is entirely optional — you can wire services explicitly and skip the container altogether.
Tokens
Tokens identify dependencies in the container. There are three kinds:
Class tokens
Derived from Go types. Most common when using constructor-based DI:
import "go.putnami.dev/inject"
token := inject.TokenOf[*UserService]()Named tokens
For non-struct values or when you need multiple instances of the same type:
apiURL := inject.Named[string]("api-url")
dbPool := inject.Named[*sql.Pool]("primary-db")Tag selectors
For resolving multiple providers at once:
plugins := inject.Tagged[Plugin]("plugin")Provider registration
Factory provider
reg := inject.Provide(
inject.TokenOf[*UserService](),
func(r inject.Resolver) (any, error) {
db, err := inject.ResolveAs[*Database](r, inject.TokenOf[*Database]())
if err != nil {
return nil, err
}
return NewUserService(db), nil
},
)Value provider
reg := inject.ProvideValue(inject.Named[string]("api-url"), "https://api.example.com")Instance provider
reg := inject.ProvideInstance(myLogger)Auto-provide (constructor-based)
The most concise registration. Constructor parameters are resolved by type, and the return value is registered by type:
// func NewUserService(db *Database) *UserService
reg := inject.AutoProvide(NewUserService)Supported constructor signatures:
func() T
func() (T, error)
func(dep1 A, dep2 B) T
func(dep1 A, dep2 B) (T, error)Auto-provide variants
// Scoped — new instance per scope (e.g., per HTTP request)
inject.AutoProvideScoped(NewRequestContext)
// Named — register under a custom name
inject.AutoProvideNamed("primary-db", NewDatabase)Provider options
Options configure provider behavior:
inject.Provide(token, factory,
inject.WithScope(inject.Scoped),
inject.WithVisibility(inject.Private),
inject.WithTags("plugin", "auth"),
inject.WithDeps(inject.TokenOf[*Database]()),
inject.WithOnClose(func() error { return db.Close() }),
inject.WithLazy(),
inject.WithDynamic(),
)| Option | Default | Description |
|---|---|---|
WithScope(Scoped) |
Singleton |
New instance per scope instead of one global instance |
WithVisibility(Private) |
Public |
Hide from child containers |
WithTags(...) |
— | Tags for multi-resolution via List() |
WithDeps(...) |
— | Explicit dependency declarations for validation |
WithOnClose(fn) |
— | Cleanup hook called on Close() |
WithLazy() |
— | Skip resolution at startup; resolve on first access |
WithDynamic() |
— | Allow refresh after startup |
Container
Creating a container
root := inject.NewContainer("root", nil)
root.Register(inject.AutoProvide(NewDatabase))
root.Register(inject.AutoProvide(NewUserService))Resolving dependencies
val, err := root.Get(inject.TokenOf[*UserService]())
userService := val.(*UserService)
// Check existence
if root.Has(inject.TokenOf[*UserService]()) {
// registered
}Hierarchy
Child containers inherit parent registrations. Private providers are not visible to children:
child := root.CreateChild("auth-module")
child.Register(inject.Provide(
inject.TokenOf[*TokenValidator](),
factory,
inject.WithVisibility(inject.Private), // only visible in this child
))Validation
issues := root.Validate()
for _, err := range issues {
fmt.Println(err) // missing deps, circular deps, scope violations
}ContainerContext
ContainerContext is the application-level entry point that adds lifecycle management and scoping on top of Container:
cc := inject.NewContainerContext("app")
cc.Register(inject.AutoProvide(NewDatabase))
cc.Register(inject.AutoProvide(NewUserService))
// Start — validates graph, resolves non-lazy singletons
if err := cc.Start(); err != nil {
log.Fatal(err)
}
// Resolve
val, _ := cc.Get(inject.TokenOf[*UserService]())
// Close — runs onClose hooks in reverse order
defer cc.Close()Modules
Mount a group of registrations as a child container with explicit requirements:
authRegs := []inject.Registration{
inject.AutoProvide(NewAuthService),
inject.AutoProvide(NewTokenValidator),
}
authReqs := []inject.Token{
inject.TokenOf[*Database](), // must be provided by parent
}
cc.Mount("auth", authRegs, authReqs)Scopes
Singleton (default)
One instance shared for the container's lifetime:
inject.AutoProvide(NewDatabase) // singleton by defaultScoped
New instance for each scope (e.g., per HTTP request):
inject.AutoProvideScoped(NewRequestContext)Using scoped services
err := cc.Scope(ctx, func(ctx context.Context, scope inject.ScopeContext) error {
reqCtx, _ := scope.Get(inject.TokenOf[*RequestContext]())
// reqCtx is unique to this scope
// Same scope = same instance
reqCtx2, _ := scope.Get(inject.TokenOf[*RequestContext]())
// reqCtx == reqCtx2
return nil
})Context-based resolution
Inside a scope, resolve from context.Context directly:
err := cc.Scope(ctx, func(ctx context.Context, scope inject.ScopeContext) error {
// Typed resolution from context
userService, err := inject.Resolve[*UserService](ctx, inject.TokenOf[*UserService]())
if err != nil {
return err
}
return userService.Process(ctx)
})Detached scopes
For scopes with manual lifetime control:
scope, err := cc.CreateScope()
if err != nil {
return err
}
defer scope.Close()
// Use the scope's context
scopedCtx := scope.Context(ctx)
userService, _ := inject.Resolve[*UserService](scopedCtx, inject.TokenOf[*UserService]())Tags and multi-resolution
Tag providers for grouped resolution:
cc.Register(inject.AutoProvide(NewPluginA, inject.WithTags("plugin")))
cc.Register(inject.AutoProvide(NewPluginB, inject.WithTags("plugin")))
// Resolve all tagged providers
vals, _ := cc.List(inject.Tagged[Plugin]("plugin"))
// vals contains [PluginA, PluginB]Inside factories, use the resolver:
inject.Provide(
inject.TokenOf[*PluginManager](),
func(r inject.Resolver) (any, error) {
plugins, _ := r.ResolveAll(inject.Tagged[Plugin]("plugin"))
return NewPluginManager(plugins), nil
},
)Fx-style DI with Application
The app.Application provides a higher-level API for constructor-based DI:
import "go.putnami.dev/app"
a := app.New("my-service")
// Register constructors — params resolved by type
a.ProvideFunc(
NewDatabase, // func() (*Database, error)
NewUserService, // func(db *Database) *UserService
NewUserHandler, // func(users *UserService) *UserHandler
)
// Scoped constructors — new instance per scope
a.ProvideScopedFunc(NewRequestContext)
// Invoke — run after DI is built
a.InvokeFunc(func(handler *UserHandler, server *fhttp.ServerPlugin) {
server.GET("/users", handler.List)
})
a.ListenAndServe()Scope proxies
When a singleton depends on a scoped provider, use a ScopeProxy to defer resolution:
// RequestID is scoped — new per request
// AuditLogger is singleton — constructed once
proxy := inject.ProvideScopeProxy[*RequestID](
inject.TokenOf[*RequestID](),
ctxFunc, // returns current context.Context
)
cc.Register(proxy)
// In the singleton, call proxy.Get() to access the current scope's instanceTesting
Fork and override
Fork a ContainerContext for test isolation:
func TestUserService(t *testing.T) {
fork := cc.Fork()
fork.Override(inject.TokenOf[*Database](), func(r inject.Resolver) (any, error) {
return NewMockDatabase(), nil
})
fork.OverrideValue(inject.TokenOf[*EmailService](), &MockEmailService{})
testCC, err := fork.Start()
if err != nil {
t.Fatal(err)
}
defer testCC.Close()
val, _ := testCC.Get(inject.TokenOf[*UserService]())
userService := val.(*UserService)
// test with mocked dependencies
}Manual test container
func TestUserService(t *testing.T) {
cc := inject.NewContainerContext("test")
cc.Register(inject.ProvideValue(inject.TokenOf[*Database](), mockDB))
cc.Register(inject.AutoProvide(NewUserService))
if err := cc.Start(); err != nil {
t.Fatal(err)
}
defer cc.Close()
val, _ := cc.Get(inject.TokenOf[*UserService]())
userService := val.(*UserService)
// test
}Validation
The container validates the entire dependency graph during Start():
- Missing dependencies — all declared deps must be registered
- Circular dependencies — detected via DFS-based graph analysis
- Scope violations — singleton depending on scoped (use scope proxies)
- Requirement validation — mounted modules' requirements must be satisfied
cc.Register(inject.AutoProvide(NewA)) // depends on B
cc.Register(inject.AutoProvide(NewB)) // depends on A
err := cc.Start()
// Error: circular dependency: *A → *B → *AError codes
| Code | Description |
|---|---|
inject.not_registered |
Token not found in container or parents |
inject.circular_dependency |
Circular dependency detected |
inject.scope_violation |
Singleton depends on scoped without proxy |
inject.container_closed |
Operation on a closed container |
inject.duplicate_provider |
Token already registered |
inject.requirement_not_met |
Module requirement not satisfied |
inject.type_mismatch |
Resolved value doesn't match expected type |
inject.factory_failed |
Factory function returned an error |
Related guides
- Plugins & Lifecycle — application-level DI
- Configuration — config-to-DI bridge
- Testing — testing with DI