gontainer

License GoDoc Test Report

Gontainer

Simple but powerful dependency injection container for Go projects!

Features

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

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.