Registering Services
Service registration is how you tell godi about your services, their constructors, and their lifetimes. This guide covers all the ways to register services.
Basic Registration
AddSingleton
Registers a service that will have a single instance throughout the application lifetime.
// Register by constructor function
services.AddSingleton(NewLogger)
// Register with explicit type
services.AddSingleton(func() Logger {
return &FileLogger{path: "/var/log/app.log"}
})
// Register instance directly
logger := &ConsoleLogger{}
services.AddSingleton(func() Logger { return logger })
AddScoped
Registers a service that will have one instance per scope.
// Typical for repository pattern
services.AddScoped(NewUserRepository)
// With dependencies
services.AddScoped(func(db *Database, logger Logger) UserRepository {
return &SqlUserRepository{db: db, logger: logger}
})
AddTransient
Registers a service that creates a new instance every time it’s resolved.
// For stateless operations
services.AddTransient(NewEmailMessage)
// Factory pattern
services.AddTransient(func() EmailMessage {
return &EmailMessage{
From: "noreply@example.com",
Timestamp: time.Now(),
}
})
Constructor Patterns
Simple Constructor
The most common pattern - a function that returns a service:
func NewUserService(repo UserRepository, logger Logger) *UserService {
return &UserService{
repo: repo,
logger: logger,
}
}
services.AddScoped(NewUserService)
Constructor with Error
Constructors can return an error as the second value:
func NewDatabase(config *Config) (*Database, error) {
db, err := sql.Open("postgres", config.DatabaseURL)
if err != nil {
return nil, fmt.Errorf("failed to connect: %w", err)
}
if err := db.Ping(); err != nil {
return nil, fmt.Errorf("failed to ping: %w", err)
}
return &Database{db: db}, nil
}
services.AddSingleton(NewDatabase)
Interface Registration
Always register services by their interface when possible:
// Good: Register by interface
func NewFileLogger(config *Config) Logger { // Returns interface
return &FileLogger{path: config.LogPath}
}
// Avoid: Register by concrete type
func NewFileLogger(config *Config) *FileLogger { // Returns concrete type
return &FileLogger{path: config.LogPath}
}
Keyed Services
Register multiple implementations of the same interface using keys:
// Register different cache implementations
services.AddSingleton(
func() Cache { return &RedisCache{} },
godi.Name("redis"),
)
services.AddSingleton(
func() Cache { return &MemoryCache{} },
godi.Name("memory"),
)
// Register named databases
services.AddSingleton(
func(config *Config) Database {
return NewPostgresDB(config.PrimaryDB)
},
godi.Name("primary"),
)
services.AddSingleton(
func(config *Config) Database {
return NewPostgresDB(config.ReadReplicaDB)
},
godi.Name("replica"),
)
Service Groups
Register multiple services that will be collected into a slice:
// Register HTTP handlers
services.AddSingleton(NewUserHandler, godi.Group("handlers"))
services.AddSingleton(NewOrderHandler, godi.Group("handlers"))
services.AddSingleton(NewProductHandler, godi.Group("handlers"))
// Consume the group
type Router struct {
godi.In
Handlers []http.Handler `group:"handlers"`
}
func NewRouter(params Router) *mux.Router {
router := mux.NewRouter()
for _, handler := range params.Handlers {
handler.RegisterRoutes(router)
}
return router
}
Instance Registration
Sometimes you need to register an existing instance:
// Configuration loaded from file
config := loadConfigFromFile("config.yaml")
services.AddSingleton(func() *Config { return config })
// Third-party client
redisClient := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
services.AddSingleton(func() *redis.Client { return redisClient })
Conditional Registration
Register services based on configuration or environment:
// Based on environment
if os.Getenv("ENV") == "production" {
services.AddSingleton(NewS3Storage)
} else {
services.AddSingleton(NewLocalStorage)
}
// Based on configuration
if config.EnableCache {
services.AddSingleton(NewRedisCache)
} else {
services.AddSingleton(NewNoOpCache)
}
// Feature flags
if config.Features.EmailNotifications {
services.AddScoped(NewEmailService)
services.AddScoped(NewNotificationService)
}
Generic Services
Register generic services with type constraints:
// Generic repository
type Repository[T any] interface {
GetByID(id string) (*T, error)
Save(entity *T) error
}
// Concrete implementation
type MongoRepository[T any] struct {
collection *mongo.Collection
}
// Register for specific types
services.AddScoped(func(db *mongo.Database) Repository[User] {
return &MongoRepository[User]{
collection: db.Collection("users"),
}
})
services.AddScoped(func(db *mongo.Database) Repository[Order] {
return &MongoRepository[Order]{
collection: db.Collection("orders"),
}
})
Registration Options
Service Replacement
Replace an existing service registration:
// Initial registration
services.AddSingleton(NewFileLogger)
// Replace with different implementation
services.Replace(godi.Singleton, NewConsoleLogger)
Remove Services
Remove service registrations:
// Remove all registrations of a type
services.RemoveAll(reflect.TypeOf((*Logger)(nil)).Elem())
// Clear all registrations
services.Clear()
Factory Pattern
Use factories for complex construction logic:
// Factory interface
type ServiceFactory interface {
CreateService(name string) (Service, error)
}
// Factory implementation
type DefaultServiceFactory struct {
logger Logger
config *Config
}
func NewServiceFactory(logger Logger, config *Config) ServiceFactory {
return &DefaultServiceFactory{
logger: logger,
config: config,
}
}
func (f *DefaultServiceFactory) CreateService(name string) (Service, error) {
switch name {
case "email":
return NewEmailService(f.logger, f.config.SMTP), nil
case "sms":
return NewSMSService(f.logger, f.config.Twilio), nil
default:
return nil, fmt.Errorf("unknown service: %s", name)
}
}
// Register the factory
services.AddSingleton(NewServiceFactory)
Best Practices
1. Register by Interface
// Good
func NewService() ServiceInterface { }
// Avoid
func NewService() *ServiceImpl { }
2. Use Appropriate Lifetimes
// Singletons: Stateless, shared resources
services.AddSingleton(NewLogger)
services.AddSingleton(NewConfiguration)
// Scoped: Request-specific, stateful
services.AddScoped(NewRepository)
services.AddScoped(NewUnitOfWork)
// Transient: Lightweight, unique state
services.AddTransient(NewCommand)
services.AddTransient(NewEmailMessage)
3. Validate Early
// Validate during registration
services.AddSingleton(func(config *Config) (Database, error) {
if config.DatabaseURL == "" {
return nil, errors.New("database URL required")
}
return NewDatabase(config)
})
4. Document Dependencies
// NewUserService creates a user service
// Dependencies:
// - UserRepository: for data access
// - Logger: for logging operations
// - EmailService: for sending notifications
func NewUserService(
repo UserRepository,
logger Logger,
email EmailService,
) *UserService {
return &UserService{
repo: repo,
logger: logger,
email: email,
}
}
Common Patterns
Options Pattern
type ServerOptions struct {
Port int
TLS bool
CertFile string
KeyFile string
}
func NewServer(logger Logger, opts ServerOptions) *Server {
return &Server{
logger: logger,
options: opts,
}
}
// Register with options
services.AddSingleton(func(logger Logger) *Server {
return NewServer(logger, ServerOptions{
Port: 8080,
TLS: false,
})
})
Multi-Stage Initialization
// Stage 1: Create instance
func NewDatabaseConnection(config *Config) *DatabaseConnection {
return &DatabaseConnection{
url: config.DatabaseURL,
}
}
// Stage 2: Initialize
func (db *DatabaseConnection) Initialize() error {
return db.connect()
}
// Register with initialization
services.AddSingleton(func(config *Config) (*DatabaseConnection, error) {
db := NewDatabaseConnection(config)
if err := db.Initialize(); err != nil {
return nil, err
}
return db, nil
})
Next Steps
Learn about Using Scopes
Explore Keyed Services in detail
Understand Service Groups
Master Modules for organization