Advanced Features
This guide covers advanced godi features for specific use cases. Most applications won’t need these, but they’re powerful when you do.
Keyed Services
Register multiple implementations of the same interface:
// Multiple database connections
var DatabaseModule = godi.NewModule("databases",
godi.AddSingleton(NewPrimaryDB, godi.Name("primary")),
godi.AddSingleton(NewReplicaDB, godi.Name("replica")),
godi.AddSingleton(NewAnalyticsDB, godi.Name("analytics")),
)
// Use specific implementation
func main() {
collection := godi.NewCollection()
collection.AddModules(DatabaseModule)
provider, _ := collection.Build()
// Get specific database
primary, _ := godi.ResolveKeyed[Database](provider, "primary")
replica, _ := godi.ResolveKeyed[Database](provider, "replica")
// Use different databases for different operations
primary.Execute("INSERT INTO users...") // Writes go to primary
replica.Query("SELECT * FROM users...") // Reads from replica
}
With Parameter Objects
type RepositoryParams struct {
godi.In
Primary Database `name:"primary"`
Replica Database `name:"replica"`
Analytics Database `name:"analytics" optional:"true"`
}
func NewUserRepository(params RepositoryParams) *UserRepository {
return &UserRepository{
primary: params.Primary,
replica: params.Replica,
analytics: params.Analytics, // Might be nil
}
}
Service Groups
Collect multiple services of the same type:
// Register validators
var ValidationModule = godi.NewModule("validation",
godi.AddSingleton(NewEmailValidator, godi.Group("validators")),
godi.AddSingleton(NewPhoneValidator, godi.Group("validators")),
godi.AddSingleton(NewAddressValidator, godi.Group("validators")),
)
// Use all validators
type ValidationService struct {
validators []Validator
}
func NewValidationService(params struct {
godi.In
Validators []Validator `group:"validators"`
}) *ValidationService {
return &ValidationService{
validators: params.Validators,
}
}
func (v *ValidationService) ValidateAll(data any) error {
for _, validator := range v.validators {
if err := validator.Validate(data); err != nil {
return err
}
}
return nil
}
Real Example: Middleware Chain
// HTTP middleware
type Middleware interface {
Wrap(http.Handler) http.Handler
}
// Register middleware
var MiddlewareModule = godi.NewModule("middleware",
godi.AddScoped(NewLoggingMiddleware, godi.Group("middleware")),
godi.AddScoped(NewAuthMiddleware, godi.Group("middleware")),
godi.AddScoped(NewRateLimitMiddleware, godi.Group("middleware")),
)
// Build middleware chain
type Server struct {
middleware []Middleware
handler http.Handler
}
func NewServer(params struct {
godi.In
Middleware []Middleware `group:"middleware"`
}) *Server {
return &Server{
middleware: params.Middleware,
}
}
func (s *Server) Start() {
handler := s.handler
// Apply middleware in reverse order
for i := len(s.middleware) - 1; i >= 0; i-- {
handler = s.middleware[i].Wrap(handler)
}
http.ListenAndServe(":8080", handler)
}
Parameter Objects (In/Out)
Input Parameters (godi.In)
Simplify constructors with many dependencies:
// Instead of this:
func NewUserService(
db Database,
cache Cache,
logger Logger,
emailService EmailService,
smsService SMSService,
config *Config,
) *UserService { }
// Use this:
type UserServiceParams struct {
godi.In
DB Database
Cache Cache `optional:"true"`
Logger Logger
EmailService EmailService
SMSService SMSService `optional:"true"`
Config *Config
}
func NewUserService(params UserServiceParams) *UserService {
svc := &UserService{
db: params.DB,
logger: params.Logger,
emailService: params.EmailService,
config: params.Config,
}
// Optional dependencies
if params.Cache != nil {
svc.cache = params.Cache
}
if params.SMSService != nil {
svc.smsService = params.SMSService
}
return svc
}
Output Parameters (godi.Out)
Register multiple services from one constructor:
type RepositoryBundle struct {
godi.Out
UserRepo UserRepository
ProductRepo ProductRepository
OrderRepo OrderRepository `name:"orders"`
}
func NewRepositories(db Database) RepositoryBundle {
return RepositoryBundle{
UserRepo: &userRepository{db: db},
ProductRepo: &productRepository{db: db},
OrderRepo: &orderRepository{db: db},
}
}
// Register once, get three services!
var DataModule = godi.NewModule("data",
godi.AddSingleton(NewDatabase),
godi.AddScoped(NewRepositories), // Registers all three repos
)
Resource Disposal
Services that implement Disposable are automatically cleaned up:
type Disposable interface {
Close() error
}
// Example: Database connection
type DatabaseConnection struct {
conn *sql.DB
}
func NewDatabaseConnection(config *Config) (*DatabaseConnection, error) {
conn, err := sql.Open("postgres", config.DSN)
if err != nil {
return nil, err
}
return &DatabaseConnection{conn: conn}, nil
}
// Implement Disposable
func (db *DatabaseConnection) Close() error {
if db.conn != nil {
return db.conn.Close()
}
return nil
}
// Automatically closed when provider/scope closes!
Disposal Order
Services are disposed in reverse order of creation:
// Creation order:
// 1. Logger
// 2. Database
// 3. Cache
// 4. Service
// Disposal order (when scope closes):
// 1. Service
// 2. Cache
// 3. Database
// 4. Logger
Scoped Disposal Example
type Transaction struct {
tx *sql.Tx
committed bool
}
func NewTransaction(db *Database) (*Transaction, error) {
tx, err := db.Begin()
if err != nil {
return nil, err
}
return &Transaction{tx: tx}, nil
}
func (t *Transaction) Commit() error {
t.committed = true
return t.tx.Commit()
}
// Auto-rollback if not committed
func (t *Transaction) Close() error {
if !t.committed && t.tx != nil {
return t.tx.Rollback()
}
return nil
}
// Transaction automatically rolls back when scope closes!
Register As Interface
Register concrete types as their interfaces:
type Cache interface {
Get(key string) (any, bool)
Set(key string, value any)
}
type RedisCache struct {
client *redis.Client
}
func NewRedisCache() *RedisCache {
return &RedisCache{}
}
func (r *RedisCache) Get(key string) (any, bool) { /* ... */ }
func (r *RedisCache) Set(key string, value any) { /* ... */ }
// Register as interface
var CacheModule = godi.NewModule("cache",
godi.AddSingleton(NewRedisCache, godi.As(new(Cache))),
)
// Resolve as interface
cache, _ := godi.Resolve[Cache](provider) // Returns RedisCache as Cache
Mixed Lifetime Groups
Groups can contain services with different lifetimes:
var ProcessorModule = godi.NewModule("processors",
// Different lifetimes in same group
godi.AddSingleton(NewMetricsProcessor, godi.Group("processors")),
godi.AddScoped(NewRequestProcessor, godi.Group("processors")),
godi.AddTransient(NewTempProcessor, godi.Group("processors")),
)
// When resolved:
// - MetricsProcessor: same instance always (singleton)
// - RequestProcessor: same instance per scope (scoped)
// - TempProcessor: new instance every time (transient)
Advanced Patterns
Factory Pattern
type ConnectionFactory struct {
config *Config
}
func NewConnectionFactory(config *Config) *ConnectionFactory {
return &ConnectionFactory{config: config}
}
func (f *ConnectionFactory) CreateConnection(database string) (Connection, error) {
switch database {
case "users":
return NewConnection(f.config.UsersDB)
case "orders":
return NewConnection(f.config.OrdersDB)
default:
return nil, errors.New("unknown database")
}
}
Lazy Loading
type LazyService struct {
initOnce sync.Once
provider godi.Provider
service *ExpensiveService
err error
}
func NewLazyService(provider godi.Provider) *LazyService {
return &LazyService{provider: provider}
}
func (l *LazyService) Get() (*ExpensiveService, error) {
l.initOnce.Do(func() {
// Only create when first needed
l.service, l.err = godi.Resolve[*ExpensiveService](l.provider)
})
return l.service, l.err
}
Context Enrichment
// Add request metadata to context
func EnrichContext(ctx context.Context, r *http.Request) context.Context {
ctx = context.WithValue(ctx, "requestID", generateID())
ctx = context.WithValue(ctx, "userID", getUserID(r))
ctx = context.WithValue(ctx, "traceID", getTraceID(r))
return ctx
}
// Services can access context values
func NewAuditLog(ctx context.Context) *AuditLog {
return &AuditLog{
requestID: ctx.Value("requestID").(string),
userID: ctx.Value("userID").(string),
traceID: ctx.Value("traceID").(string),
}
}
Performance Considerations
Singleton vs Scoped
Singleton: Created once at startup, fastest resolution
Scoped: Created once per scope, cached within scope
Transient: Created every time, slowest but ensures uniqueness
Minimizing Allocations
// Use pointer receivers for large structs
type LargeService struct {
data [1000]byte
}
func (s *LargeService) DoWork() { } // Pointer receiver
// Pool transient objects
var bufferPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
func NewBuffer() *bytes.Buffer {
return bufferPool.Get().(*bytes.Buffer)
}
When to Use Advanced Features
Keyed Services: Multiple implementations (databases, APIs)
Service Groups: Plugin systems, middleware chains
Parameter Objects: Constructors with 4+ parameters
Disposal: Resources that need cleanup (files, connections)
As Interface: Decouple from concrete types
Summary
These advanced features are powerful but not always necessary. Start simple and add complexity only when you need it. The beauty of godi is that these features are there when you need them, but don’t get in the way when you don’t!