Simple but powerful dependency injection container for Go projects!

See the complete webapp example for this in action.
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)
}
}
12:51:32 Executing service container
12:51:32 Hello from the Hello Service Bob
12:51:32 Service container executed
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
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
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
go get github.com/NVIDIA/gontainer/v2
Requirements: Go 1.21+
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
}
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()
}
})
err := gontainer.Run(
gontainer.NewFactory(...),
gontainer.NewFactory(...),
gontainer.NewEntrypoint(func(/* dependencies */) {
// application entry point
}),
)
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)
}
})
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
})
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
})
A factory can depend on an interface, and the container injects a registered service that implements it:
type Store interface {
Get(key string) (string, bool)
}
gontainer.NewFactory(func() *MemoryStore {
return &MemoryStore{} // implements Store
})
gontainer.NewFactory(func(store Store) *API {
return &API{store: store}
})
When several services implement the same interface, a regular dependency resolves to the first one in registration order, and only that implementation is spawned - others are left untouched. This is by design: it keeps regular resolution deterministic and free of side effects. To receive every implementation, depend on
Multiple[T]instead.
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")}
})
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()
})
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
})
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)
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 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)
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
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
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.
}
Gontainer treats a panic as a bug, not a control-flow mechanism, and never recovers
from one. A panic in a factory, an entrypoint, or a cleanup callback propagates
straight out of Run and crashes the process loudly, with the full stack trace
intact.
Expected failures should be returned as an error - which the container renders as
the structured traceback shown above - while a panic signals a programming error that
you want to surface immediately rather than silently absorb.
We welcome contributions! Please see CONTRIBUTING.md for guidelines.
Apache 2.0 – See LICENSE for details.
v1Documentation for the previous major version v1 is available at v1 branch.