A lightweight dependency injection container for Go. Register constructors, build once, resolve anywhere — with full type safety via generics.
- Constructor injection — dependencies are expressed as function parameters
- Generics-first API —
oak.Resolve[*DB](c)with compile-time type safety - Singleton & Transient lifetimes — one shared instance or a fresh one every time
- Named providers — multiple implementations of the same type
- Circular dependency detection — caught at build time with full chain in the error
- Graceful shutdown — auto-closes
io.Closersingletons in reverse dependency order - Concurrency safe — thread-safe resolution after build
- Zero dependencies — only the Go standard library
go get github.com/ARTM2000/oakRequires Go 1.21 or later.
package main
import (
"fmt""log""github.com/ARTM2000/oak"
)
typeLoggerstruct{ Prefixstring }
typeConfigstruct{ DSNstring }
typeDatabasestruct {
Config*ConfigLogger*Logger
}
funcmain() {
c:=oak.New()
// Register constructors — order does not matter.c.Register(func() *Config { return&Config{DSN: "postgres://localhost/app"} })
c.Register(func() *Logger { return&Logger{Prefix: "app"} })
c.Register(func(cfg*Config, log*Logger) *Database {
return&Database{Config: cfg, Logger: log}
})
// Build validates the graph and creates all singletons.iferr:=c.Build(); err!=nil {
log.Fatal(err)
}
// Resolve with full type safety.db, err:= oak.Resolve[*Database](c)
iferr!=nil {
log.Fatal(err)
}
fmt.Println(db.Config.DSN) // postgres://localhost/appfmt.Println(db.Logger.Prefix) // app
}| Lifetime | Behaviour |
|---|---|
Singleton | Created once during Build(). Same instance returned on every Resolve(). This is the default. |
Transient | A new instance is constructed on every Resolve() call. |
c.Register(NewLogger, oak.WithLifetime(oak.Transient))When you need several implementations of the same return type, register them by name:
c.RegisterNamed("mysql", NewMySQLDB)
c.RegisterNamed("postgres", NewPostgresDB)
// later …db, _:=oak.ResolveNamed[*sql.DB](c, "postgres")Named providers create a new instance on every ResolveNamed call. Their
dependencies are resolved from the typed provider pool.
Build() does three things:
- Validates the entire dependency graph — missing providers and circular dependencies are caught here, not at runtime.
- Instantiates all singleton providers eagerly.
- Locks the container — no further registrations are accepted.
Singleton providers that implement io.Closer
are automatically tracked during Build(). Call Shutdown(ctx) to close them
in reverse dependency order — dependents are closed before their
dependencies:
// Database implements io.Closerfunc (db*Database) Close() error {
returndb.pool.Close()
}
// After you're done:ctx, cancel:=context.WithTimeout(context.Background(), 5*time.Second)
defercancel()
iferr:=c.Shutdown(ctx); err!=nil {
log.Println("shutdown error:", err)
}Key behaviours:
- Only singleton providers are tracked — transient instances are the caller's responsibility.
- If a
Close()call returns an error, shutdown continues and all errors are joined in the result. - If the context expires, remaining closers are skipped and the context error is included.
- Calling
Shutdowntwice returnsErrAlreadyShutdown.
Tip: Any type can opt into automatic cleanup by implementing io.Closer.
If your type doesn't naturally have a Close method, add one:
typeCachestruct { /* ... */ }
func (c*Cache) Close() error {
c.Flush()
returnnil
}Constructors must be functions with one of these return signatures:
func(deps...) Tfunc(deps...) (T, error)If a constructor returns (T, error) and the error is non-nil, Build()
(for singletons) or Resolve() (for transients) will propagate it.
| Function / Method | Description |
|---|---|
oak.New() Container | Create a new empty container |
c.Register(ctor, opts...) error | Register a typed constructor |
c.RegisterNamed(name, ctor, opts...) error | Register a named constructor |
c.Build() error | Validate graph and instantiate singletons |
oak.Resolve[T](c) (T, error) | Resolve a type (generic, recommended) |
oak.ResolveNamed[T](c, name) (T, error) | Resolve a named provider (generic, recommended) |
c.Resolve(reflect.Type) (reflect.Value, error) | Resolve by reflect.Type |
c.ResolveNamed(name, reflect.Type) (reflect.Value, error) | Resolve named by reflect.Type |
c.Shutdown(ctx) error | Close all io.Closer singletons |
| Option | Description |
|---|---|
oak.WithLifetime(oak.Transient) | Set the provider lifetime (default Singleton) |
All errors can be checked with errors.Is:
| Error | When |
|---|---|
oak.ErrNotBuilt | Resolve called before Build |
oak.ErrAlreadyBuilt | Register or Build called after Build |
oak.ErrProviderNotFound | No provider for the requested type or name |
oak.ErrCircularDependency | Dependency graph contains a cycle |
oak.ErrDuplicateProvider | Same type or name registered twice |
oak.ErrAlreadyShutdown | Shutdown called more than once |
See _examples/userapp for a runnable example that
wires up a small layered application.
Contributions are welcome! Please read CONTRIBUTING.md before opening a pull request.