gontainer

package module
v2.4.1 Latest Latest
Warning

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

Go to latest
Published: Apr 20, 2026 License: Apache-2.0, BSD-2-Clause, BSD-3-Clause, + 2 more Imports: 7 Imported by: 6

README

License GoDoc Test Report

Gontainer

Simple but powerful dependency injection container for Go projects!

Features

  • 🎯 Automatic dependency injection based on function signatures.
  • ✨ Super simple interface to register and run services.
  • 🚀 Lazy service creation only when actually needed.
  • 🔄 Lifecycle management with proper cleanup in reverse order.
  • 🤖 Clean and tested implementation using reflection and generics.
  • 🧩 No external packages, no code generation, zero dependencies.

Quick Start

The example shows how to build the simplest app using service container.

package main

import (
    "log"
    "github.com/NVIDIA/gontainer/v2"
)

// Your services.
type Database struct{ connString string }
type UserService struct{ db *Database }

func main() {
    err := gontainer.Run(
        // Register Database.
        gontainer.NewFactory(func() *Database {
            return &Database{connString: "postgres://localhost/myapp"}
        }),
        
        // Register UserService - Database is auto-injected!
        gontainer.NewFactory(func(db *Database) *UserService {
            return &UserService{db: db}
        }),
        
        // Use your services.
        gontainer.NewEntrypoint(func(users *UserService) {
            log.Printf("UserService ready with DB: %s", users.db)
        }),
    )
    
    if err != nil {
        log.Fatal(err)
    }
}

Examples

  • Console command example – demonstrates how to build a simple console command.
    12:51:32 Executing service container
    12:51:32 Hello from the Hello Service Bob
    12:51:32 Service container executed
    
  • Daemon service example – demonstrates how to maintain background services.
    12:48:22 Executing service container
    12:48:22 Starting listening on: http://127.0.0.1:8080
    12:48:22 Starting serving HTTP requests
    ------ Application was started and now accepts HTTP requests -------------
    ------ CTRL+C was pressed or a TERM signal was sent to the process -------
    12:48:28 Exiting from serving by signal
    12:48:28 Service container executed
    
  • Complete webapp example – demonstrates how to organize web application with multiple services.
    15:19:48 INFO msg="Starting service container" service=logger
    15:19:48 INFO msg="Configuring app endpoints" service=app
    15:19:48 INFO msg="Configuring health endpoints" service=app
    15:19:48 INFO msg="Starting HTTP server" service=http address=127.0.0.1:8080
    ------ Application was started and now accepts HTTP requests -------------
    15:19:54 INFO msg="Serving home page" service=app remote-addr=127.0.0.1:62640
    15:20:01 INFO msg="Serving health check" service=app remote-addr=127.0.0.1:62640
    ------ CTRL+C was pressed or a TERM signal was sent to the process -------
    15:20:04 INFO msg="Terminating by signal" service=app
    15:20:04 INFO msg="Closing HTTP server" service=http
    
  • Transient service example – demonstrates how to return a function that can be called multiple times to produce transient services.
    11:19:22 Executing service container
    11:19:22 New value: 8767488676555705225
    11:19:22 New value: 5813207273458254863
    11:19:22 New value: 750077227530805093
    11:19:22 Service container executed
    

Installation

go get github.com/NVIDIA/gontainer/v2

Requirements: Go 1.21+

Core Concepts

1. Define Services

Services are just regular Go types:

type EmailService struct {
    smtp string
}

func (s *EmailService) SendWelcome(email string) error {
    log.Printf("Sending welcome email to %s via %s", email, s.smtp)
    return nil
}
2. Register Factories

Factories create your services. Dependencies are declared as function parameters:

// Simple factory.
gontainer.NewFactory(func() *EmailService {
    return &EmailService{smtp: "smtp.gmail.com"}
})

// Factory with dependencies - auto-injected!
gontainer.NewFactory(func(config *Config, logger *Logger) *EmailService {
    logger.Info("Creating email service")
    return &EmailService{smtp: config.SMTPHost}
})

// Factory with a cleanup callback.
gontainer.NewFactory(func() (*Database, func() error) {
    db, _ := sql.Open("postgres", "...")
    
    return db, func() error {
        log.Println("Closing database")
        return db.Close()
    }
})
3. Run Container
err := gontainer.Run(
    gontainer.NewFactory(...),
    gontainer.NewFactory(...),
    gontainer.NewEntrypoint(func(/* dependencies */) {
        // application entry point
    }),
)

Advanced Features

Resource Cleanup

Return a cleanup function from your factory to handle graceful shutdown:

gontainer.NewFactory(func() (*Server, func() error) {
    server := &http.Server{Addr: ":8080"}
    go server.ListenAndServe()
    
    // Cleanup function called on container shutdown.
    return server, func() error {
        ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
        defer cancel()
        return server.Shutdown(ctx)
    }
})
Optional Dependencies

Use when a service might not be registered:

gontainer.NewFactory(func(metrics gontainer.Optional[*MetricsService]) *API {
    api := &API{}
    
    // Use metrics if available
    if m := metrics.Get(); m != nil {
        api.metrics = m
    }
    
    return api
})
Multiple Dependencies

Get all services implementing an interface:

type Middleware interface {
    Process(http.Handler) http.Handler
}

gontainer.NewFactory(func(middlewares gontainer.Multiple[Middleware]) *Router {
    router := &Router{}
    for _, mw := range middlewares {
        router.Use(mw)
    }
    return router
})
Multiple Instances of the Same Type

The container matches services by exact type. To register several instances of the same underlying type, give each one a distinct named type (compile-time) or group them in a composite service (runtime):

// Compile-time: the set of instances is known at build time.
// Typos and wiring mistakes are caught by the compiler.
type UsersDB  *sql.DB
type OrdersDB *sql.DB

gontainer.NewFactory(func(c *Config) (UsersDB, func() error) {
    db, _ := sql.Open("postgres", c.UsersDSN)
    return db, db.Close
})

gontainer.NewFactory(func(c *Config) (OrdersDB, func() error) {
    db, _ := sql.Open("postgres", c.OrdersDSN)
    return db, db.Close
})

gontainer.NewFactory(func(u UsersDB, o OrdersDB) *Service {
    return &Service{users: u, orders: o}
})
// Runtime: the set of instances comes from configuration.
// Access is stringly-typed but flexible.
type DBs struct{ byAlias map[string]*sql.DB }

func (d *DBs) Get(alias string) *sql.DB { return d.byAlias[alias] }

gontainer.NewFactory(func(c *Config) (*DBs, func() error) {
    open := make(map[string]*sql.DB, len(c.Databases))
    for alias, dsn := range c.Databases {
        db, _ := sql.Open("postgres", dsn)
        open[alias] = db
    }
    return &DBs{byAlias: open}, func() error {
        var errs []error
        for _, db := range open {
            errs = append(errs, db.Close())
        }
        return errors.Join(errs...)
    }
})

gontainer.NewFactory(func(dbs *DBs) *Service {
    return &Service{users: dbs.Get("users")}
})
Dynamic Resolution

Resolve services on-demand:

gontainer.NewEntrypoint(func(resolver *gontainer.Resolver) error {
    // Resolve service dynamically.
    var userService *UserService
    if err := resolver.Resolve(&userService); err != nil {
        return err
    }
    
    return userService.DoWork()
})
Transient Services

Create new instances on each call:

// Factory returns a function that creates new instances.
gontainer.NewFactory(func(db *Database) func() *Transaction {
    return func() *Transaction {
        return &Transaction{
            id: uuid.New(),
            db: db,
        }
    }
})

// Use the factory function.
gontainer.NewEntrypoint(func(newTx func() *Transaction) {
    tx1 := newTx()  // new instance
    tx2 := newTx()  // another new instance
})
Factory Annotations

Attach arbitrary metadata to a factory or entrypoint with WithAnnotation. Annotations are exposed via Factory.Annotations() / Entrypoint.Annotations() and can be read without starting the container - useful for --help, config validation, CLI dispatch, or any pre-run tooling built on top of the same factory definitions.

type cliHelp struct {
    Cmd string
    Doc string
}

configFactory := gontainer.NewFactory(
    newConfig,
    gontainer.WithAnnotation(cliHelp{Cmd: "config", Doc: "Print resolved config"}),
)

dbFactory := gontainer.NewFactory(
    newDatabase,
    gontainer.WithAnnotation(cliHelp{Cmd: "db", Doc: "Ping the database"}),
)

// Inspect annotations without starting the container.
for _, f := range []*gontainer.Factory{configFactory, dbFactory} {
    for _, a := range f.Annotations() {
        if h, ok := a.(cliHelp); ok {
            fmt.Printf("%s\t%s\n", h.Cmd, h.Doc)
        }
    }
}

// Start the container with the same factories when ready.
_ = gontainer.Run(configFactory, dbFactory, entrypoint)

API Reference

Module Functions

Gontainer module interface is really simple:

// Run creates and runs a container with provided factories and entrypoints.
func Run(options ...Option) error

// NewFactory registers a service factory.
func NewFactory(fn any) *Factory

// NewService registers a pre-created service.
func NewService[T any](service T) *Factory

// NewEntrypoint registers an entrypoint function.
func NewEntrypoint(fn any) *Entrypoint
Factory Signatures

Factory is a function that creates one service. It can have dependencies as parameters, and can optionally return an error and/or a cleanup function for the factory.

Dependencies are other services that the factory needs which are automatically injected.

Service is a user-provided type. It can be any type except untyped any and error.

// The simplest factory.
func() *Service

// Factory with dependencies.
func(dep1 *Dep1, dep2 *Dep2) *Service

// Factory with error.
func() (*Service, error)

// Factory with cleanup.
func() (*Service, func() error)

// Factory with cleanup and error.
func() (*Service, func() error, error)
Built-in Services

Gontainer provides several built-in services that can be injected into factories and functions. They provide access to container features like dynamic resolution and invocation.

// *gontainer.Resolver - Dynamic service resolution.
func(resolver *gontainer.Resolver) *Service

// *gontainer.Invoker - Dynamic function invocation.
func(invoker *gontainer.Invoker) *Service
Special Types

Gontainer provides special types for declaring optional and multiple dependencies in factory and entrypoint signatures. See Optional Dependencies and Multiple Dependencies for full examples.

// Optional[T] - declares a dependency that may be absent from the container.
// Call .Get() to read the value; the zero value of T is returned when no
// matching factory is registered.
func(logger gontainer.Optional[*Logger]) *Service

// Multiple[T] - declares a dependency on all services assignable to T.
// Range over the slice to access each registered service.
func(providers gontainer.Multiple[AuthProvider]) *Router

Error Handling

Container errors are rendered as a structured traceback: the root cause on the first line, followed by resolution frames with file:line references.

Startup error - error returned by a factory:

Configuration load failed:
 - DATABASE_USERNAME: required environment variable is not set
 - DATABASE_PASSWORD: required environment variable is not set

Traceback:
  Factory for *myapp.Config
    at /path/to/app/config.go:18
  Factory for *myapp.Database
    at /path/to/app/db.go:24
  Entrypoint
    at /path/to/app/main.go:15

Close error - errors returned from close callbacks:

connection reset by peer

Source:
  Factory for *myapp.Database
    at /path/to/app/db.go:24

Typed errors are also exposed for programmatic matching:

err := gontainer.Run(factories...)

switch {
case errors.Is(err, gontainer.ErrFactoryReturnedError):
    // Factory returned an error.
case errors.Is(err, gontainer.ErrEntrypointReturnedError):
    // Entrypoint returned an error.
case errors.Is(err, gontainer.ErrNoEntrypointsProvided):
    // No entrypoints were provided.
case errors.Is(err, gontainer.ErrCircularDependency):
    // Circular dependency detected.
case errors.Is(err, gontainer.ErrDependencyNotResolved):
    // Service type not registered.
case errors.Is(err, gontainer.ErrFactoryTypeDuplicated):
    // Service type was duplicated.
}

Contributing

We welcome contributions! Please see CONTRIBUTING.md for guidelines.

License

Apache 2.0 – See LICENSE for details.

Documentation for v1

Documentation for the previous major version v1 is available at v1 branch.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrCircularDependency = errors.New("circular dependency")

ErrCircularDependency declares a circular dependency error.

View Source
var ErrDependencyNotResolved = errors.New("dependency not resolved")

ErrDependencyNotResolved declares service not resolved error.

View Source
var ErrEntrypointReturnedError = errors.New("entrypoint returned error")

ErrEntrypointReturnedError declares entrypoint returned error.

View Source
var ErrFactoryReturnedError = errors.New("factory returned error")

ErrFactoryReturnedError declares factory returned error.

View Source
var ErrFactoryTypeDuplicated = errors.New("factory type duplicated")

ErrFactoryTypeDuplicated declares service duplicated error.

View Source
var ErrNoEntrypointsProvided = errors.New("no entrypoints provided")

ErrNoEntrypointsProvided declares no entrypoints provided error.

Functions

func Run

func Run(options ...Option) error

Run runs a container with a set of configured factories.

Run registers the provided options, validates the registry, invokes entrypoints synchronously, and then tears down all spawned factories in reverse order. It returns when all entrypoints have returned and teardown has completed.

func WithAnnotation added in v2.2.0

func WithAnnotation(value any) annotationOpt

WithAnnotation returns an option that attaches a value to a Factory or Entrypoint.

Types

type Entrypoint added in v2.1.0

type Entrypoint struct {
	// contains filtered or unexported fields
}

Entrypoint is a container option that registers an entrypoint function.

func NewEntrypoint

func NewEntrypoint(function any, opts ...EntrypointOption) *Entrypoint

NewEntrypoint creates a new factory which will be called by the container.

Example:

gontainer.NewEntrypoint(func(db *Database) error { ... })
gontainer.NewEntrypoint(func(db *Database) { ... })

func (*Entrypoint) Annotations added in v2.2.0

func (e *Entrypoint) Annotations() []any

Annotations returns a copy of the associated annotations.

func (*Entrypoint) Name added in v2.2.0

func (e *Entrypoint) Name() string

Name returns the human-readable name of the entrypoint.

func (*Entrypoint) Source added in v2.2.0

func (e *Entrypoint) Source() string

Source returns the source package path of the entrypoint.

type EntrypointOption added in v2.2.0

type EntrypointOption interface {
	// contains filtered or unexported methods
}

EntrypointOption is an option for configuring an Entrypoint.

type Factory added in v2.1.0

type Factory struct {
	// contains filtered or unexported fields
}

Factory is a container option that registers a service factory or singleton.

func NewFactory

func NewFactory(function any, opts ...FactoryOption) *Factory

NewFactory creates a new service load using the provided load function.

The load function must be a function. It may accept dependencies as input parameters and return exactly one service instances, optionally followed by an error as the second return value.

Example:

gontainer.NewFactory(func(db *Database) *Handler { ... })
gontainer.NewFactory(func(db *Database) (*Handler, error) { ... })
gontainer.NewFactory(func(db *Database) (*Handler, func() error) { ... })
gontainer.NewFactory(func(db *Database) (*Handler, func() error, error) { ... })

func NewService

func NewService[T any](service T, opts ...FactoryOption) *Factory

NewService creates a new service load that always returns the given singleton value.

This is a convenience helper for registering preconstructed service instances as factories. The returned load produces the same instance on every invocation.

This is useful for registering constants, mocks, or externally constructed values.

Example:

logger := NewLogger()
gontainer.NewService(logger)

func (*Factory) Annotations added in v2.2.0

func (f *Factory) Annotations() []any

Annotations returns a copy of the associated annotations.

func (*Factory) Name added in v2.2.0

func (f *Factory) Name() string

Name returns the human-readable name of the factory.

func (*Factory) Source added in v2.2.0

func (f *Factory) Source() string

Source returns the source package path of the factory.

type FactoryOption added in v2.2.0

type FactoryOption interface {
	// contains filtered or unexported methods
}

FactoryOption is an option for configuring a Factory or a Service.

type Invoker

type Invoker struct {
	// contains filtered or unexported fields
}

Invoker invokes functions with automatic dependency resolution.

The Invoke method accepts a function `fn`, resolves its input parameters using the invoker's dependency resolver, and then calls the function with the resolved arguments.

If the container has not been started yet, dependency resolution happens in lazy mode — only the required arguments and their transitive dependencies are instantiated on demand.

The Invoke method returns:

  • []any - all values returned by the function (including any errors)
  • error - only if dependency resolution fails or fn is not a function

All return values from the invoked function are collected in the []any slice, including any error values. The caller is responsible for checking and handling these values as appropriate.

func (*Invoker) Invoke

func (i *Invoker) Invoke(function any) ([]any, error)

Invoke invokes specified function.

type Multiple

type Multiple[T any] []T

Multiple defines a dependency on zero or more services of the same type.

This generic wrapper is used in service factory function parameters to declare a dependency on all services assignable to type T registered in the container.

The container will collect and inject all matching services into the slice. For interface types, multiple matches are allowed. For concrete (non-interface) types, at most one match is possible.

Example:

func MyFactory(providers gontainer.Multiple[AuthProvider]) {
    for _, p := range providers {
        ...
    }
}

type Option

type Option interface {
	// contains filtered or unexported methods
}

Option is the interface for container options.

type Optional

type Optional[T any] struct {
	// contains filtered or unexported fields
}

Optional defines a dependency on a service that may or may not be registered.

This generic wrapper is used in service factory function parameters to declare that the service of type T is optional. If the container does not contain a matching service, the zero value of T will be injected.

Use the Get() method to access the wrapped value inside the factory.

Example:

func MyFactory(logger gontainer.Optional[Logger]) {
    if log := logger.Get(); log != nil {
        log.Info("Logger available")
    }
}

func (*Optional[T]) Get

func (o *Optional[T]) Get() T

Get returns the optional service instance.

func (*Optional[T]) Ok added in v2.3.0

func (o *Optional[T]) Ok() bool

Ok reports whether the optional service was provided by the container.

type Resolver

type Resolver struct {
	// contains filtered or unexported fields
}

Resolver resolves service dependencies.

The Resolve method accepts a pointer to a variable (`varPtr`) and attempts to populate it with an instance of the requested type. The type is determined via reflection based on the element type of `varPtr`.

If the container has not been started yet, Resolve operates in lazy mode — it instantiates only the requested type and its transitive dependencies on demand.

An error is returned if the service of the requested type is not found or cannot be resolved.

func (*Resolver) Resolve

func (r *Resolver) Resolve(varPtr any) error

Resolve sets the required dependency via the pointer.

Jump to

Keyboard shortcuts

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