This package is an autowiring and DI injection library. Designed to make use of generics and type safe resolvers. It is a container based DI framework. That means that you will need access to the container to being able to resolve dependencies.
-
Circular dependencies detection: Thanks to the usage of graphs and DFS this library is able to detect circular dependencies. That detection is meant to be produced early avoiding to crash on production. Circular dependencies should be detected while developing the app.
-
Type safe resolving: The container only holds dependencies all the logic of resolving is encapsulated in a
container.Resolvefunction that manages the type detection successfully.
- Reflection: If you need an extreme performance this library is not for you. All the execution of adding and resolving dependencies is done with reflection. That consumes time and CPU, which is not ideal for ultra performant services.
First you need to create a container and declare dependencies. There are two lifetimes:
- Singleton: The resolver runs once and the result is cached for subsequent resolutions.
- Transient: The resolver runs every time the dependency is resolved.
import (
"bytes"
"log/slog"
"os"
"github.com/4strodev/wiring_graphs/pkg/container"
)
func main() {
cont := container.New()
// Must() returns a wrapper that panics on errors, allowing method chaining.
cont.Must().
// Singleton: resolved once and cached.
Singleton(func() *slog.Logger {
return slog.New(slog.NewJSONHandler(os.Stdout, nil))
}).
// Transient: a new instance is created on every resolution.
Dependencies(func() *bytes.Buffer {
return bytes.NewBufferString("hello")
})
// You can also register without Must(), handling errors explicitly.
err := cont.Singleton(func() *bytes.Buffer {
return bytes.NewBufferString("hello")
})
if err != nil {
panic(err)
}
}Resolvers can depend on other registered types. Input parameters are resolved automatically by the container:
cont.Must().
Singleton(func() *slog.Logger {
return slog.New(slog.NewJSONHandler(os.Stdout, nil))
}).
// The *slog.Logger parameter is resolved from the container.
Dependencies(func(logger *slog.Logger) *bytes.Buffer {
logger.Info("creating buffer")
return bytes.NewBufferString("hello")
})Dependencies can also be registered by token (a string key) instead of by type. This is useful when you need multiple values of the same type:
cont.Must().
// Register by token (transient).
Token(map[string]any{
"stdout": func() *bytes.Buffer { return bytes.NewBufferString("stdout") },
"stderr": func() *bytes.Buffer { return bytes.NewBufferString("stderr") },
}).
// Register by token (singleton).
TokenSingleton(map[string]any{
"config": func() *bytes.Buffer { return bytes.NewBufferString("config data") },
})Use the generic Resolve and ResolveToken package-level functions to retrieve
dependencies from the container. Circular dependencies are automatically detected
before resolution.
import (
"bytes"
"fmt"
"log/slog"
"github.com/4strodev/wiring_graphs/pkg/container"
)
func main() {
cont := container.New()
cont.Must().
Singleton(func() *slog.Logger {
return slog.New(slog.NewJSONHandler(os.Stdout, nil))
}).
Token(map[string]any{
"buffer": func() *bytes.Buffer { return bytes.NewBufferString("hello") },
})
// Resolve by type.
logger, err := container.Resolve[*slog.Logger](cont)
if err != nil {
panic(err)
}
logger.Info("resolved logger")
// Resolve by token.
buf, err := container.ResolveToken[*bytes.Buffer](cont, "buffer")
if err != nil {
panic(err)
}
fmt.Println(buf.String()) // hello
}You can auto-populate a struct's exported fields from the container using Fill().
Fields are resolved by type by default. Use the wiring struct tag to resolve by
token or to skip a field.
Tag format: wiring:"tokenName,omit"
| Tag | Behavior |
|---|---|
| (no tag) | Resolve by field type |
wiring:"myToken" |
Resolve by token "myToken" |
wiring:",omit" |
Skip this field |
Unexported fields are always skipped automatically.
import (
"bytes"
"fmt"
"log/slog"
"os"
"github.com/4strodev/wiring_graphs/pkg/container"
)
// Define a struct with optional wiring tags.
type AppDeps struct {
Logger *slog.Logger // resolved by type
Buffer *bytes.Buffer `wiring:"appBuffer"` // resolved by token "appBuffer"
Skipped *bytes.Buffer `wiring:",omit"` // explicitly skipped
private *bytes.Buffer // unexported, automatically skipped
}
func main() {
cont := container.New()
cont.Must().
Singleton(func() *slog.Logger {
return slog.New(slog.NewJSONHandler(os.Stdout, nil))
}).
Token(map[string]any{
"appBuffer": func() *bytes.Buffer { return bytes.NewBufferString("filled!") },
})
// Fill takes a pointer to a struct.
var deps AppDeps
err := cont.Fill(&deps)
if err != nil {
panic(err)
}
deps.Logger.Info("logger resolved via Fill")
fmt.Println(deps.Buffer.String()) // filled!
fmt.Println(deps.Skipped == nil) // true
fmt.Println(deps.private == nil) // true
}Fill can also be used inside a resolver to inject multiple dependencies at once:
type ServiceDeps struct {
Logger *slog.Logger
Buffer *bytes.Buffer `wiring:"buffer"`
}
cont.Must().
Singleton(
func() *slog.Logger {
return slog.New(slog.NewJSONHandler(os.Stdout, nil))
},
// The *Container is automatically available for injection.
func(c *container.Container) *MyService {
var deps ServiceDeps
if err := c.Fill(&deps); err != nil {
panic(err)
}
return NewMyService(deps.Logger, deps.Buffer)
},
).
Token(map[string]any{
"buffer": func() *bytes.Buffer { return bytes.NewBufferString("data") },
})