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"
)
type Logger struct{ Prefix string }
type Config struct{ DSN string }
type Database struct {
Config *Config
Logger *Logger
}
func main() {
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.
if err := c.Build(); err != nil {
log.Fatal(err)
}
// Resolve with full type safety.
db, err := oak.Resolve[*Database](c)
if err != nil {
log.Fatal(err)
}
fmt.Println(db.Config.DSN) // postgres://localhost/app
fmt.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.Closer
func (db *Database) Close() error {
return db.pool.Close()
}
// After you're done:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := 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:
type Cache struct { /* ... */ }
func (c *Cache) Close() error {
c.Flush()
return nil
}Constructors must be functions with one of these return signatures:
func(deps...) T
func(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.