Compiler Passes
Compiler passes transform configuration before code generation, enabling project-specific conventions and patterns.
Overview
Compiler passes allow you to programmatically modify the DI configuration before container generation. This enables:
- Auto-tagging by naming conventions
- Service registration from code analysis
- Argument transformation (e.g., adding logging)
- Validation of custom constraints
- Configuration normalization
When to Use Passes
Use compiler passes when you need:
- Project-specific conventions (e.g., auto-tag all
*Handlerservices) - Dynamic service registration from external sources
- Custom validation rules beyond type checking
- Configuration preprocessing (e.g., environment variable expansion)
- Code generation from annotations
When NOT to Use Passes
Don’t use passes for:
- Simple configuration changes (use YAML imports/overrides instead)
- Type transformations (use decorators instead)
- Runtime behavior modification (implement in your services)
Pass Interface
All compiler passes implement the Pass interface:
package di
type Pass interface {
Name() string
Process(cfg *Config) (*Config, error)
}Interface Methods
Name() string
- Returns a unique identifier for the pass
- Used in error messages and logging
- Should be lowercase with hyphens (e.g.,
"auto-tag")
Process(cfg *Config) (*Config, error)
- Receives the current configuration
- Returns the transformed configuration
- Returns an error if transformation fails
- Mutates the config in place and returns it for chaining
Creating a Pass
Basic Pass Structure
package passes
import (
di "github.com/gendi-org/gendi"
)
type MyPass struct {
// Optional: configuration fields
}
func (p *MyPass) Name() string {
return "my-pass"
}
func (p *MyPass) Process(cfg *di.Config) (*di.Config, error) {
// Transform cfg
// Return modified config or error
return cfg, nil
}The worked examples below fill this skeleton in — see Auto-Tagging by Convention for a complete pass.
Modifying the Configuration
The Config struct contains three maps:
type Config struct {
Parameters map[string]Parameter
Tags map[string]Tag
Services map[string]Service
}Safe modification pattern:
func (p *MyPass) Process(cfg *di.Config) (*di.Config, error) {
// Iterate over services
for id, svc := range cfg.Services {
// Modify service
svc.Tags = append(svc.Tags, di.ServiceTag{
Name: "my-tag",
})
// Write back to map (services are value types)
cfg.Services[id] = svc
}
return cfg, nil
}Important: Services, parameters, and tags are value types. Always write back to the map after modification.
Adding Services
func (p *MyPass) Process(cfg *di.Config) (*di.Config, error) {
cfg.Services["new_service"] = di.Service{
Constructor: di.Constructor{
Func: "github.com/myapp.NewService",
Args: []di.Argument{
{Kind: di.ArgLiteral, Literal: di.NewStringLiteral("value")},
},
},
Shared: true,
}
return cfg, nil
}Adding Tags
func (p *MyPass) Process(cfg *di.Config) (*di.Config, error) {
cfg.Tags["my-tag"] = di.Tag{
ElementType: "github.com/myapp.Handler",
SortBy: "priority",
}
return cfg, nil
}Adding Parameters
func (p *MyPass) Process(cfg *di.Config) (*di.Config, error) {
cfg.Parameters["new_param"] = di.Parameter{
Value: di.NewStringLiteral("default-value"),
}
return cfg, nil
}Common Use Cases
Auto-Tagging by Convention
Tag services based on naming patterns:
type AutoTagPass struct{}
func (p *AutoTagPass) Name() string {
return "auto-tag"
}
func (p *AutoTagPass) Process(cfg *di.Config) (*di.Config, error) {
for id, svc := range cfg.Services {
// Tag all services ending with ".handler"
if strings.HasSuffix(id, ".handler") {
svc.Tags = append(svc.Tags, di.ServiceTag{
Name: "http.handler",
})
cfg.Services[id] = svc
}
// Tag all services starting with "middleware."
if strings.HasPrefix(id, "middleware.") {
svc.Tags = append(svc.Tags, di.ServiceTag{
Name: "http.middleware",
})
cfg.Services[id] = svc
}
}
return cfg, nil
}Adding Logging to Services
Automatically inject logger into all services:
type LoggingPass struct{}
func (p *LoggingPass) Name() string {
return "auto-logging"
}
func (p *LoggingPass) Process(cfg *di.Config) (*di.Config, error) {
for id, svc := range cfg.Services {
// Skip logger itself
if id == "logger" {
continue
}
// Add logger as first argument
svc.Constructor.Args = append(
[]di.Argument{{Kind: di.ArgServiceRef, Value: "logger"}},
svc.Constructor.Args...,
)
cfg.Services[id] = svc
}
return cfg, nil
}Custom Validation
Validate custom business rules:
type ValidationPass struct{}
func (p *ValidationPass) Name() string {
return "validation"
}
func (p *ValidationPass) Process(cfg *di.Config) (*di.Config, error) {
// Ensure all public services are shared
for id, svc := range cfg.Services {
if svc.Public && !svc.Shared {
return nil, fmt.Errorf(
"service %q is public but not shared", id,
)
}
}
// Ensure required services exist
requiredServices := []string{"logger", "database"}
for _, required := range requiredServices {
if _, exists := cfg.Services[required]; !exists {
return nil, fmt.Errorf(
"required service %q not found", required,
)
}
}
return cfg, nil
}Priority-Based Ordering
Automatically assign priorities based on registration order:
type PriorityPass struct{}
func (p *PriorityPass) Name() string {
return "auto-priority"
}
func (p *PriorityPass) Process(cfg *di.Config) (*di.Config, error) {
priority := 1000
for id, svc := range cfg.Services {
// Add priority to all handler tags
for i, tag := range svc.Tags {
if tag.Name == "handler" && tag.Attributes["priority"] == nil {
if svc.Tags[i].Attributes == nil {
svc.Tags[i].Attributes = make(map[string]interface{})
}
svc.Tags[i].Attributes["priority"] = priority
priority += 10
}
}
cfg.Services[id] = svc
}
return cfg, nil
}Environment-Based Configuration
Load configuration from environment:
type EnvPass struct{}
func (p *EnvPass) Name() string {
return "env-config"
}
func (p *EnvPass) Process(cfg *di.Config) (*di.Config, error) {
// Override parameters from environment
for name, param := range cfg.Parameters {
envKey := "APP_" + strings.ToUpper(
strings.ReplaceAll(name, ".", "_"),
)
if envValue := os.Getenv(envKey); envValue != "" {
param.Value = di.NewStringLiteral(envValue)
cfg.Parameters[name] = param
}
}
return cfg, nil
}Best Practices
1. Keep Passes Focused
Each pass should do one thing well:
// ✅ Good: Focused pass
type AutoTagHandlerPass struct{}
// ❌ Bad: Does too much
type AutoConfigureEverythingPass struct{}2. Document Pass Behavior
Add clear documentation:
// AutoTagPass automatically tags services by naming convention:
// - Services ending with ".handler" → "http.handler" tag
// - Services starting with "middleware." → "http.middleware" tag
type AutoTagPass struct{}3. Fail Fast with Clear Errors
Return descriptive errors:
if svc.Constructor.Func == "" && svc.Constructor.Method == "" {
return nil, fmt.Errorf(
"service %q: missing constructor (required by auto-tag pass)",
id,
)
}4. Don’t Assume Configuration State
Validate your assumptions:
func (p *MyPass) Process(cfg *di.Config) (*di.Config, error) {
if cfg.Services == nil {
cfg.Services = make(map[string]di.Service)
}
// Now safe to modify cfg.Services
// ...
}5. Use Source Locations
Add source location tracking for better error messages:
svc.Constructor.SourceLoc = &srcloc.Location{
File: "auto-generated",
Line: 0,
}6. Test Your Passes
Write unit tests:
func TestAutoTagPass(t *testing.T) {
cfg := &di.Config{
Services: map[string]di.Service{
"home.handler": {
Constructor: di.Constructor{
Func: "app.NewHomeHandler",
},
},
},
}
pass := &AutoTagPass{}
result, err := pass.Process(cfg)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
svc := result.Services["home.handler"]
if len(svc.Tags) != 1 || svc.Tags[0].Name != "http.handler" {
t.Errorf("expected http.handler tag, got: %v", svc.Tags)
}
}7. Order Matters
Passes run in sequence. Be aware of dependencies:
customPasses := []di.Pass{
&NormalizationPass{}, // Run first: normalize config
&AutoTagPass{}, // Then: add tags
&ValidationPass{}, // Finally: validate result
}If these passes are registered with cmd.Run, use []di.Pass.
API Reference
The types a pass works with — Config, Service, Constructor,
Argument/ArgumentKind, Tag, ServiceTag, Parameter — and the
NewStringLiteral/NewIntLiteral/NewFloatLiteral/NewBoolLiteral/NewNullLiteral
constructors are declared in config.go and literal.go of the root di
package. Read them there or on
pkg.go.dev rather
than from a copy that can drift.
Two properties worth knowing before writing a pass:
- Every config struct carries an optional
SourceLoc *srcloc.Location. Keep it when you rewrite an entry — it is what puts the offending YAML line into generation errors Packagesfields are derived, not authored: the pipeline recomputes them after passes run, so a pass never has to maintain them