Container is a lightweight yet powerful IoC (dependency injection) container for Go projects. It's built neat, easy-to-use, and performance-in-mind to be your ultimate requirement.
Features:
- Singleton and Transient bindings
- Named dependencies (bindings)
- Scoped containers (a nested scope tree sharing a single root)
- Resolve by functions, variables, and structs
- Must helpers that convert errors to panics
- Optional lazy loading of bindings
- Global instance for small applications
- Concurrency-safe with no race conditions
- Circular dependency detection, backed by a graph of the bindings
- Dependency graph visualization in the Graphviz DOT format
- Bind-time and resolve-time parameter injection
- 100% Test coverage!
It requires Go v1.26 or newer versions.
To install this package, run the following command in your project directory.
go get github.com/danceable/containerNext, include it in your application:
import "github.com/danceable/container"Container works by binding abstractions (interfaces) to their concrete implementations via resolver functions.
You register a binding with Bind(), passing a resolver function that returns the concrete type, along with optional configuration:
- Singleton (
bind.Singleton()): The resolver is called once; the same instance is returned for every subsequent request. - Transient (default): The resolver is called on every request, producing a new instance each time.
- Named (
bind.WithName("...")) : Multiple concretes can be registered for the same abstraction under different names. - Lazy (
bind.Lazy()): Defers the resolver invocation until the binding is first resolved.
Once bindings are registered, you can retrieve concretes through:
Resolve(&target)— fills a variable with the bound concrete.Call(fn)— invokes a function whose parameters are automatically resolved from the container.Fill(&struct)— injects dependencies into struct fields tagged withcontainer:"type"orcontainer:"name".
Your code depends on abstractions, not implementations!
The following example demonstrates a simple binding and resolving.
err := container.Bind(func() Config {
return &JsonConfig{...}
}, bind.Singleton())
var c Config
err = container.Resolve(&c)The package provides a default global Container instance — exposed as container.Default — for convenience in small applications. Instead of creating a container with container.New(), you can call container.Bind(), container.Resolve(), container.Call(), container.Fill(), and container.Reset() directly as package-level functions; they all delegate to container.Default.
You can also access container.Default directly if you need to pass the global instance to a function or a Must helper.
// No need to create a container — uses the global instance (container.Default)
container.Bind(func() Database {
return &MySQL{Host: "localhost"}
}, bind.Singleton())
var db Database
container.Resolve(&db)
container.Call(func(db Database) {
db.Connect()
})
// Pass the global instance to a Must helper
container.MustBind(container.Default, func() Cache {
return &RedisCache{}
}, bind.Singleton())
// Reset clears all bindings from the global instance
container.Reset()A singleton binding creates one shared instance. The resolver is called once, and every subsequent resolve returns the same object.
c := container.New()
err := c.Bind(func() Database {
return &MySQL{Host: "localhost", Port: 3306}
}, bind.Singleton())
var db1, db2 Database
// db1 and db2 point to the same instance
c.Resolve(&db1)
c.Resolve(&db2)A transient binding (the default) calls the resolver on every resolve, producing a fresh instance each time.
c := container.New()
err := c.Bind(func() Logger {
return &FileLogger{Path: "/var/log/app.log"}
})
var l1, l2 Logger
// l1 and l2 are different instances
c.Resolve(&l1)
c.Resolve(&l2)Named bindings allow registering multiple concretes for the same abstraction under different names.
c := container.New()
c.Bind(func() Database {
return &MySQL{Host: "primary"}
}, bind.Singleton(), bind.WithName("primary"))
c.Bind(func() Database {
return &MySQL{Host: "replica"}
}, bind.Singleton(), bind.WithName("replica"))A lazy binding defers resolver invocation until the first time the binding is resolved, rather than at bind time. This is useful when a dependency isn't always needed or is expensive to create.
c := container.New()
err := c.Bind(func() Cache {
return NewRedisCache("localhost:6379") // not called until first resolve
}, bind.Singleton(), bind.Lazy())An eager binding (the default) invokes the resolver immediately at bind time to validate it. For singletons, this also creates and caches the instance right away.
c := container.New()
// The resolver runs immediately — any error is returned by Bind.
err := c.Bind(func() Database {
return &MySQL{Host: "localhost"}
}, bind.Singleton())Use resolve.WithName() to retrieve a specific named binding during Resolve, Call, or Fill.
c := container.New()
c.Bind(func() Database {
return &MySQL{Host: "primary"}
}, bind.Singleton(), bind.WithName("primary"))
c.Bind(func() Database {
return &MySQL{Host: "replica"}
}, bind.Singleton(), bind.WithName("replica"))
var replica Database
c.Resolve(&replica, resolve.WithName("replica"))Use resolve.WithParams() to supply values at resolve time. These are matched by type to the resolver's arguments and take precedence over container bindings.
Important: When using resolve.WithParams(), bind with bind.Lazy() so the resolver is not invoked until the parameters are available at resolve time. Without bind.Lazy(), an eager binding will fail if parameters cannot be resolved at bind time.
c := container.New()
// Bind with Lazy() so the resolver isn't called until resolve time,
// when we have the DSN parameter available
c.Bind(func(dsn string) Database {
return &MySQL{DSN: dsn}
}, bind.Lazy())
var db Database
// Provide the DSN value at resolve time
c.Resolve(&db, resolve.WithParams("user:pass@tcp(localhost)/mydb"))
// db is now a MySQL instance with the provided DSNUse bind.ResolveDepenenciesByParams() to lock in specific parameter values at bind time. These take precedence over container lookups but can still be overridden by resolve.WithParams().
c := container.New()
c.Bind(func(timeout int) Cache {
return &RedisCache{Timeout: timeout}
}, bind.Lazy(), bind.ResolveDepenenciesByParams(30))
var cache Cache
c.Resolve(&cache) // resolver receives timeout=30Use bind.ResolveDependenciesByNamedBindings() to wire a resolver's arguments to specific named bindings instead of the default (unnamed) ones.
c := container.New()
c.Bind(func() Database {
return &MySQL{Host: "replica"}
}, bind.WithName("replica"), bind.Singleton(), bind.Lazy())
c.Bind(func(db Database) ReportService {
return &Reporter{DB: db}
}, bind.Lazy(), bind.ResolveDependenciesByNamedBindings("replica"))
var svc ReportService
c.Resolve(&svc) // Reporter receives the "replica" DatabaseScope() derives a nested child container that shares the same root scope, forming a scope tree. A scope can resolve bindings registered on itself or on any of its ancestors, while its own bindings stay invisible to ancestor and sibling scopes. This is useful for layering request- or task-scoped dependencies on top of long-lived application-wide ones.
root := container.New()
// Application-wide singleton, visible to every scope in the tree.
root.Bind(func() Database {
return &MySQL{Host: "localhost"}
}, bind.Singleton())
// A nested scope — e.g. per request.
request := root.Scope("request")
// Bindings on the child shadow ancestors and stay local to this scope.
request.Bind(func() Logger {
return &RequestLogger{ID: "req-123"}
}, bind.Singleton())
var db Database
request.Resolve(&db) // resolved from the root scope
var log Logger
request.Resolve(&log) // resolved from the request scopeA binding's dependencies are resolved from the scope where the binding was registered. A service bound on the root therefore never sees services that live only in a descendant scope. Calling Scope() with a name that already exists on a scope returns the existing child, so the tree never holds duplicate siblings. The package-level container.Scope() derives a scope from the global container.Default.
root := container.New()
a := root.Scope("a")
b := root.Scope("a") // a == b — same child returned
a.Root() // == root
a.Parent() // == root
a.Name() // "a"When a resolver function has arguments, the container resolves them using multiple sources. If the same argument type is available from more than one source, the following precedence applies (highest to lowest):
- Resolve-time params (
resolve.WithParams()) — values passed when callingResolveorCall. - Bind-time params (
bind.ResolveDepenenciesByParams()) — values locked in at binding time. - Named bindings (
bind.ResolveDependenciesByNamedBindings()) — values pulled from named container entries. - Container lookup — the default unnamed binding for the matching type.
A resolver argument of an interface type falls back to a bound type that implements it when the interface itself is not bound. When several bound types implement it, the one registered first answers — every time, so the same argument never resolves to a different binding from one call to the next.
Resolve() and Fill() do not fall back this way: they match the type exactly, so Resolve(&shape) needs a binding for Shape itself and does not settle for a bound *Circle that implements it. Bind the abstraction when you want it resolved directly.
c := container.New()
// 4. Container lookup (lowest priority)
c.Bind(func() Shape { return &Circle{Area: 99} }, bind.Singleton(), bind.Lazy())
// 3. Named binding
c.Bind(func() Shape { return &Circle{Area: 5} }, bind.WithName("special"), bind.Singleton(), bind.Lazy())
// Resolver with bind-time params (2) and named bindings (3) configured
c.Bind(func(x int, s Shape) Database {
return &PostgreSQL{X: x, Area: s.GetArea()}
}, bind.Lazy(),
bind.ResolveDepenenciesByParams(10), // 2. bind-time param for int
bind.ResolveDependenciesByNamedBindings("special"), // 3. named binding for Shape
)
var db Database
// Without resolve-time params: int=10 (bind-time), Shape.Area=5 (named binding)
c.Resolve(&db)
// With resolve-time params (highest priority): overrides both int and Shape
c.Resolve(&db, resolve.WithParams(42, &Circle{Area: 77}))The container keeps the registrations of a scope as a directed graph: every binding is a node, and every dependency it cannot satisfy on its own is an edge pointing at the binding that satisfies it. Bind() walks that graph from the binding it is about to register, and refuses it when the walk leads back to where it started — so a cycle is caught at bind time, before anything is ever built. The error tells you the path of the cycle:
c := container.New()
c.Bind(func(d Database) Shape { return &Circle{} }, bind.Lazy()) // fine: no Database is registered yet
err := c.Bind(func(s Shape) Database { return &MySQL{} }, bind.Lazy()) // closes the loop
errors.Is(err, containerErrors.ErrCircularDependency) // true
err.Error() // "container: circular dependency detected: main.Database -> main.Shape -> main.Database"The graph only holds the dependencies the container actually resolves. Arguments supplied at bind time with bind.ResolveDepenenciesByParams() are not edges, so they break a cycle instead of forming one, and edges follow the same lookup the container performs at resolve time — the named bindings given with bind.ResolveDependenciesByNamedBindings() first, then the name of the binding itself. A refused binding is rolled back: the container is left exactly as it was.
Visualize() writes the same graph in the Graphviz DOT format, which is handy for seeing what an application actually wired up:
var buf bytes.Buffer
if err := c.Visualize(&buf); err != nil {
log.Fatal(err)
}
os.WriteFile("container.dot", buf.Bytes(), 0o600)
// dot -Tsvg container.dot -o container.svgdigraph container {
rankdir = LR;
node [shape = box, style = rounded, fontname = "Helvetica"];
edge [fontname = "Helvetica"];
subgraph cluster_0 {
label = "root";
n0 [label = "main.Database\nsingleton, resolved"];
n1 [label = "main.Shape\ntransient"];
}
subgraph cluster_1 {
label = "scope \"request\"";
n2 [label = "main.Logger\nsingleton"];
}
n1 -> n0;
n2 -> n0;
}Every binding becomes a node labelled with what it provides and how — named or not, singleton or transient, already built or not — and every dependency becomes an edge. Each scope becomes a cluster, and edges cross them: the graph covers the scope it is called on, the ancestors it resolves from, and its named descendants. A dependency no binding satisfies — one passed at resolve time, or a missing one — is drawn dashed.
| Method | Signature | Description |
|---|---|---|
New |
New() *Container |
Creates a new container instance (a root scope). |
Scope |
Scope(name string) *Container |
Derives a nested child scope that shares the same root scope. Resolves bindings from itself and its ancestors. Returns the existing child if the name is already taken. |
Name |
Name() string |
Returns the scope's name (empty for a root scope). |
Root |
Root() *Container |
Returns the root scope of the tree. |
Parent |
Parent() *Container |
Returns the enclosing scope, or nil for a root scope. |
Bind |
Bind(resolver any, opts ...bind.BindOption) error |
Registers a resolver function that maps an abstraction to its concrete implementation. |
Resolve |
Resolve(abstraction any, opts ...resolve.ResolveOption) error |
Fills a pointer-to-interface (or pointer-to-type) with the matching concrete from the container. |
Call |
Call(function any, opts ...resolve.ResolveOption) error |
Invokes a function whose parameters are automatically resolved from the container. The function may optionally return an error. |
Fill |
Fill(structure any, opts ...resolve.ResolveOption) error |
Injects dependencies into struct fields tagged with container:"type" or container:"name". |
Visualize |
Visualize(w io.Writer) error |
Writes the dependency graph of the container to w in the Graphviz DOT format. |
Reset |
Reset() |
Removes all bindings and empties the container. |
Each method also has a Must variant (MustBind, MustResolve, MustCall, MustFill) that panics on error instead of returning it:
// These two are equivalent:
err := c.Bind(func() Database { return &MySQL{} }, bind.Singleton())
if err != nil { panic(err) }
container.MustBind(c, func() Database { return &MySQL{} }, bind.Singleton())Options passed to Bind() to configure how a binding behaves.
| Option | Description |
|---|---|
bind.Singleton() |
Marks the binding as a singleton — the resolver is called once, and the same instance is returned on every resolve. |
bind.Lazy() |
Defers resolver invocation until the binding is first resolved. Without this, the resolver runs eagerly at bind time. |
bind.WithName(name) |
Assigns a name to the binding, allowing multiple concretes for the same abstraction. |
bind.ResolveDepenenciesByParams(params...) |
Provides concrete values at bind time to satisfy the resolver's arguments (matched by type). |
bind.ResolveDependenciesByNamedBindings(names...) |
Specifies named bindings to use when resolving the resolver's arguments. Each argument takes the first of these names that has a binding for its type. |
c.Bind(func() Database {
return &MySQL{Host: "replica"}
}, bind.Singleton(), bind.Lazy(), bind.WithName("replica"))Options passed to Resolve(), Call(), or Fill() to customize how a binding is looked up and invoked.
| Option | Description |
|---|---|
resolve.WithName(name) |
Selects a specific named binding instead of the default (unnamed) one. |
resolve.WithParams(params...) |
Supplies runtime values to satisfy the resolver's arguments (matched by type). These take the highest precedence. |
var db Database
c.Resolve(&db, resolve.WithName("replica"), resolve.WithParams("custom-dsn"))Every container method (Bind, Resolve, Call, Fill) has a corresponding Must variant that panics instead of returning an error. These are package-level functions that accept the container as the first argument. They are useful in application setup code where a failed binding or resolution indicates a programming error that should halt execution immediately.
| Function | Wraps | Description |
|---|---|---|
MustBind(c, resolver, opts...) |
c.Bind(...) |
Registers a binding or panics. |
MustResolve(c, abstraction, opts...) |
c.Resolve(...) |
Resolves a dependency or panics. |
MustCall(c, function, opts...) |
c.Call(...) |
Calls a function with injected dependencies or panics. |
MustFill(c, structure, opts...) |
c.Fill(...) |
Fills struct fields or panics. |
c := container.New()
// Panics if the binding fails
container.MustBind(c, func() Database {
return &MySQL{Host: "localhost"}
}, bind.Singleton())
// Panics if the resolution fails
var db Database
container.MustResolve(c, &db)
// Panics if the call fails
container.MustCall(c, func(db Database) {
db.Connect()
})
// Panics if filling fails
type App struct {
DB Database `container:"type"`
}
var app App
container.MustFill(c, &app)