injector
injector is a command-line tool that generates type-safe dependency injection (DI) code for Go projects.
Unlike traditional DI frameworks that rely on provider sets or complex wiring DSLs, injector uses Go’s type system and a minimal marker tag to describe dependency injection in a clear and explicit way.
The core idea is with-error:
- The Container declares what is injected.
- Providers declare how values are constructed.
Install
Install injector using go install:
go install github.com/mickamy/injector@latest
Core Concepts
1. Container
A Container is a struct that declares the components your application exposes. Fields marked with the inject tag are managed by injector.
type Container struct {
Service service.User `inject:""`
}
- The
inject tag is purely a marker.
- It carries no value or configuration by default.
- It simply signals: "this field is managed by injector."
2. Providers
A provider is any top-level function that constructs a value.
func NewUser(database *infra.Database) User {
return &user{database: database}
}
- Providers are automatically discovered during static analysis.
- Dependencies are inferred from function parameters.
- No manual registration or wiring code is required.
Providers may optionally return an error as their second return value:
func NewDatabase(cfg config.Database) (*Database, error) {
// initialization logic
}
- If a provider returns
(T, error), injector treats it as a valid provider for T.
- Errors are propagated through the generated code and must be handled by the caller.
Must Mode
By default, injector generates constructors that return (*Container, error) so the caller can handle initialization failures.
For application entrypoints where initialization failures should immediately stop the process, you can enable Must mode:
injector generate --must ./...
Must mode generates an additional MustNew* constructor for each container.
Default behavior: panic
If a provider returns an error, MustNew* will panic by default (following the common Go convention used by functions like regexp.MustCompile).
func MustNewContainer() *Container {
c, err := NewContainer()
if err != nil {
panic(err)
}
return c
}
Optional: log.Fatal
If you prefer exiting with a log message instead of panicking, choose the strategy explicitly:
injector generate --must --on-error=fatal ./...
This changes the generated MustNew* function to use log.Fatal on error.
Note: --on-error is only valid when used with --must.
Why a Marker Tag?
The marker-only inject tag serves several important purposes:
- Makes injected fields explicit and auditable.
- Prevents accidental injection of unrelated struct fields.
- Clearly identifies the struct acting as the Container.
- Provides a foundation for future extensions without breaking compatibility.
The tag is intentionally minimal:
Service service.User `inject:""`
No values. No DSL. No complex configuration.
Interface-First Design
injector is designed to work naturally with interfaces, allowing you to expose only interfaces in your Container while keeping implementations private.
type User interface {
Register(name, password string) error
}
type user struct {
DB infra.Database
}
func (u *user) Register(name string, password string) error {
log.Printf("insert user %s with password %s\n", name, password)
return nil
}
func NewUser(db infra.Database) (User, error) {
return &user{DB: db}, nil
}
- The concrete type remains unexported.
- The provider may return
(Interface, error).
- The Container depends only on the interface, promoting decoupling.
Full Example
See the full example in the example directory.
Code Generation
Run the generator:
injector generate ./...
This produces code similar to the following:
func NewContainer() (*Container, error) {
cfg := config.NewDatabase()
db, err := infra.NewDatabase(cfg)
if err != nil {
return nil, err
}
srv, err := service.NewUser(db)
if err != nil {
return nil, err
}
return &Container{
Service: srv,
}, nil
}
Provider Selection
By default, injector selects a provider by its return type. If exactly one provider returns the required type, it is used automatically.
When multiple providers exist for the same type, you can explicitly select one using the provider directive.
type Container struct {
Service service.UserService `inject:"provider:service.NewUser"`
}
provider:<FuncName> specifies the exact constructor function to use.
- The provider’s first return type must match the field type.
- Dependencies are still automatically resolved from the provider’s parameters.
Provider Overrides for Internal Components
Sometimes a dependency required by another component has multiple providers, even if that dependency itself is not exposed by the Container. In such cases, you can declare a provider override in the Container using a blank (_) field.
type Container struct {
_ infra.Database `inject:"provider:infra.NewReaderDatabase"`
User service.UserService `inject:""`
}
- The blank field does not expose the component publicly.
- It defines a global provider selection rule for that specific type within the container.
- Any resolution requiring
infra.Database will now use NewReaderDatabase.
This keeps provider selection centralized while preserving a clean public API.
Dependency Resolution Rules
License
MIT