Skip to content
/ oak Public

Lightweight dependency injection for Go — register constructors, resolve with generics, zero dependencies.

License

Notifications You must be signed in to change notification settings

ARTM2000/oak

Repository files navigation

Oak

Go Reference CI Go Report Card License: MIT

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

About

Lightweight dependency injection for Go — register constructors, resolve with generics, zero dependencies.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors