oak

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 26, 2026 License: MIT Imports: 7 Imported by: 0

README

Oak

Go Reference CI Go Report Card

A lightweight dependency injection container for Go. Register constructors, build once, resolve anywhere — with full type safety via generics.

Features

  • Constructor injection — dependencies are expressed as function parameters
  • Generics-first APIoak.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.Closer singletons in reverse dependency order
  • Concurrency safe — thread-safe resolution after build
  • Zero dependencies — only the Go standard library

Installation

go get github.com/ARTM2000/oak

Requires Go 1.21 or later.

Quick Start

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
}

Concepts

Lifetimes
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))
Named Providers

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 Phase

Build() does three things:

  1. Validates the entire dependency graph — missing providers and circular dependencies are caught here, not at runtime.
  2. Instantiates all singleton providers eagerly.
  3. Locks the container — no further registrations are accepted.
Graceful Shutdown

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 Shutdown twice returns ErrAlreadyShutdown.

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
}
Constructor Signatures

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.

API Overview

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
Options
Option Description
oak.WithLifetime(oak.Transient) Set the provider lifetime (default Singleton)
Sentinel Errors

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

Examples

See _examples/userapp for a runnable example that wires up a small layered application.

Contributing

Contributions are welcome! Please read CONTRIBUTING.md before opening a pull request.

License

MIT

Documentation

Overview

Package oak provides a lightweight, reflection-based dependency injection container for Go.

Oak uses constructor functions to wire dependencies automatically. Register constructors with the container, call [Container.Build] to validate the dependency graph, then retrieve fully-assembled objects with Resolve or ResolveNamed.

Quick Start

c := oak.New()
c.Register(NewLogger)
c.Register(NewDatabase)
c.Build()

db, err := oak.Resolve[*Database](c)

Lifetimes

Singleton (default) — one shared instance for the lifetime of the container.

Transient — a fresh instance on every [Container.Resolve] call.

c.Register(NewLogger, oak.WithLifetime(oak.Transient))

Named Providers

When you need several implementations of the same return type, use named registration:

c.RegisterNamed("mysql", NewMySQLDB)
c.RegisterNamed("postgres", NewPostgresDB)

db, _ := oak.ResolveNamed[Database](c, "postgres")

Graceful Shutdown

Singleton providers that implement io.Closer are automatically tracked during [Container.Build]. Call [Container.Shutdown] to close them in reverse dependency order:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := c.Shutdown(ctx); err != nil {
    log.Println("shutdown error:", err)
}

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrNotBuilt is returned when Resolve is called before Build.
	ErrNotBuilt = errors.New("container not built")

	// ErrAlreadyBuilt is returned when Register or Build is called after the
	// container has already been built.
	ErrAlreadyBuilt = errors.New("container already built")

	// ErrProviderNotFound is returned when no provider is registered for the
	// requested type or name.
	ErrProviderNotFound = errors.New("provider not found")

	// ErrCircularDependency is returned when the dependency graph contains a
	// cycle. The error message includes the full chain.
	ErrCircularDependency = errors.New("circular dependency detected")

	// ErrDuplicateProvider is returned when a provider for the same type or
	// name is registered more than once.
	ErrDuplicateProvider = errors.New("duplicate provider")

	// ErrAlreadyShutdown is returned when [Container.Shutdown] is called
	// more than once.
	ErrAlreadyShutdown = errors.New("container already shut down")
)

Functions

func Resolve

func Resolve[T any](c Container) (T, error)

Resolve is a generic helper that resolves a typed provider from the container. It is the recommended way to retrieve values:

db, err := oak.Resolve[*Database](c)
Example
package main

import (
	"fmt"

	"github.com/ARTM2000/oak"
)

// Types used in examples only.
type Logger struct{ Prefix string }
type Config struct{ DSN string }
type Database struct {
	Config *Config
	Logger *Logger
}

func main() {
	c := oak.New()
	_ = c.Register(func() *Config { return &Config{DSN: "postgres://localhost"} })
	_ = c.Register(func() *Logger { return &Logger{Prefix: "app"} })
	_ = c.Register(func(cfg *Config, log *Logger) *Database {
		return &Database{Config: cfg, Logger: log}
	})
	_ = c.Build()

	db, err := oak.Resolve[*Database](c)
	if err != nil {
		panic(err)
	}
	fmt.Println(db.Config.DSN)
	fmt.Println(db.Logger.Prefix)
}
Output:

postgres://localhost
app

func ResolveNamed

func ResolveNamed[T any](c Container, name string) (T, error)

ResolveNamed is a generic helper that resolves a named provider from the container:

db, err := oak.ResolveNamed[*Database](c, "primary")
Example
package main

import (
	"fmt"

	"github.com/ARTM2000/oak"
)

type Greeter interface {
	Greet() string
}
type englishGreeter struct{}

func (g *englishGreeter) Greet() string { return "hello" }

type spanishGreeter struct{}

func (g *spanishGreeter) Greet() string { return "hola" }

func main() {
	c := oak.New()
	_ = c.RegisterNamed("en", func() Greeter { return &englishGreeter{} })
	_ = c.RegisterNamed("es", func() Greeter { return &spanishGreeter{} })
	_ = c.Build()

	en, _ := oak.ResolveNamed[Greeter](c, "en")
	es, _ := oak.ResolveNamed[Greeter](c, "es")
	fmt.Println(en.Greet())
	fmt.Println(es.Greet())
}
Output:

hello
hola

Types

type Container

type Container interface {
	// Register adds a constructor to the container. The constructor must be a
	// function with the signature func(deps...) T or func(deps...) (T, error).
	// Dependencies are expressed as function parameters and resolved by type.
	Register(constructor interface{}, opts ...Option) error

	// RegisterNamed adds a named constructor. Named providers live in a
	// separate namespace and are resolved via [Container.ResolveNamed] or
	// the generic [ResolveNamed] helper.
	RegisterNamed(name string, constructor interface{}, opts ...Option) error

	// Build validates the full dependency graph — detecting missing providers
	// and circular dependencies — and eagerly instantiates all [Singleton]
	// providers. After Build succeeds the container is immutable; no further
	// registrations are accepted.
	Build() error

	// Resolve returns the value for the given type. For [Singleton] providers
	// the cached instance is returned; for [Transient] providers a new
	// instance is constructed on each call. Prefer the generic [Resolve]
	// helper over calling this method directly.
	Resolve(t reflect.Type) (reflect.Value, error)

	// ResolveNamed returns the value for the named provider. The requested
	// type t must be assignable from the provider's return type. Prefer the
	// generic [ResolveNamed] helper over calling this method directly.
	ResolveNamed(name string, t reflect.Type) (reflect.Value, error)

	// Shutdown gracefully closes all singleton providers that implement
	// [io.Closer], in reverse dependency order (dependents are closed before
	// their dependencies). The context controls the overall deadline; if it
	// expires, remaining closers are skipped and the context error is
	// included in the result.
	//
	// Shutdown is safe to call multiple times; subsequent calls return
	// [ErrAlreadyShutdown]. It is the caller's responsibility to stop
	// calling [Container.Resolve] before or during shutdown.
	Shutdown(ctx context.Context) error
}

Container defines the interface for the dependency injection container. Use New to create an instance.

func New

func New() Container

New creates an empty Container ready for registration.

Example
package main

import (
	"fmt"

	"github.com/ARTM2000/oak"
)

// Types used in examples only.
type Logger struct{ Prefix string }

func main() {
	c := oak.New()

	_ = c.Register(func() *Logger { return &Logger{Prefix: "app"} })
	if err := c.Build(); err != nil {
		panic(err)
	}

	logger, _ := oak.Resolve[*Logger](c)
	fmt.Println(logger.Prefix)
}
Output:

app

type Lifetime

type Lifetime int

Lifetime controls how many instances of a provider the container creates.

const (
	// Singleton is the default lifetime. The constructor is called once during
	// [Container.Build] and the resulting instance is reused for every
	// subsequent [Container.Resolve] call.
	Singleton Lifetime = iota

	// Transient means a new instance is constructed on every
	// [Container.Resolve] call.
	Transient
)

func (Lifetime) String

func (l Lifetime) String() string

String returns the human-readable name of the lifetime.

type Option

type Option func(*provider)

Option configures a provider during registration.

func WithLifetime

func WithLifetime(l Lifetime) Option

WithLifetime sets the Lifetime of the provider. The default is Singleton.

Example
package main

import (
	"fmt"

	"github.com/ARTM2000/oak"
)

// Types used in examples only.
type Logger struct{ Prefix string }

func main() {
	c := oak.New()
	_ = c.Register(
		func() *Logger { return &Logger{Prefix: "app"} },
		oak.WithLifetime(oak.Transient),
	)
	_ = c.Build()

	l1, _ := oak.Resolve[*Logger](c)
	l2, _ := oak.Resolve[*Logger](c)
	fmt.Println(l1 == l2)
}
Output:

false

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL