Parameter Objects
Parameter objects (using godi.In and godi.Out) provide a structured way to handle multiple dependencies and return values in your constructors.
Understanding Parameter Objects
Parameter objects solve several problems:
Constructor functions with many parameters
Optional dependencies
Grouped dependencies
Named/keyed dependencies
Multiple return values from a single constructor
Input Parameter Objects (In)
Basic Usage
Instead of multiple parameters:
// Without parameter objects - gets unwieldy
func NewOrderService(
db *sql.DB,
logger Logger,
cache Cache,
emailer EmailService,
payment PaymentGateway,
inventory InventoryService,
) *OrderService {
return &OrderService{
db: db,
logger: logger,
cache: cache,
emailer: emailer,
payment: payment,
inventory: inventory,
}
}
Use a parameter object:
// With parameter object - cleaner and extensible
type OrderServiceParams struct {
godi.In
DB *sql.DB
Logger Logger
Cache Cache
Emailer EmailService
Payment PaymentGateway
Inventory InventoryService
}
func NewOrderService(params OrderServiceParams) *OrderService {
return &OrderService{
db: params.DB,
logger: params.Logger,
cache: params.Cache,
emailer: params.Emailer,
payment: params.Payment,
inventory: params.Inventory,
}
}
Optional Dependencies
Mark dependencies as optional:
type ServiceParams struct {
godi.In
DB *sql.DB
Logger Logger `optional:"true"`
Cache Cache `optional:"true"`
Tracer Tracer `optional:"true"`
}
func NewService(params ServiceParams) *Service {
svc := &Service{
db: params.DB,
}
// Use default logger if not provided
if params.Logger != nil {
svc.logger = params.Logger
} else {
svc.logger = NewDefaultLogger()
}
// Cache is truly optional
svc.cache = params.Cache // might be nil
return svc
}
Named Dependencies
Use named services with parameter objects:
type DatabaseParams struct {
godi.In
Primary Database `name:"primary"`
Replica Database `name:"replica"`
Analytics Database `name:"analytics" optional:"true"`
}
func NewRepository(params DatabaseParams) *Repository {
repo := &Repository{
primary: params.Primary,
replica: params.Replica,
}
// Analytics DB is optional
if params.Analytics != nil {
repo.analytics = params.Analytics
}
return repo
}
Groups in Parameters
Collect multiple services of the same type:
type ApplicationParams struct {
godi.In
Config *Config
Logger Logger
Handlers []http.Handler `group:"routes"`
Middleware []Middleware `group:"middleware"`
Validators []Validator `group:"validators"`
}
func NewApplication(params ApplicationParams) *Application {
app := &Application{
config: params.Config,
logger: params.Logger,
}
// Register all handlers
for _, handler := range params.Handlers {
app.RegisterHandler(handler)
}
// Apply all middleware
for _, mw := range params.Middleware {
app.Use(mw)
}
// Register validators
for _, v := range params.Validators {
app.AddValidator(v)
}
return app
}
Output Result Objects (Out)
Basic Usage
Return multiple services from one constructor:
type RepositoryResults struct {
godi.Out
UserRepo UserRepository
OrderRepo OrderRepository
ProductRepo ProductRepository
}
func NewRepositories(db *sql.DB, logger Logger) RepositoryResults {
return RepositoryResults{
UserRepo: NewUserRepository(db, logger),
OrderRepo: NewOrderRepository(db, logger),
ProductRepo: NewProductRepository(db, logger),
}
}
// Register once, get three services
collection.AddSingleton(NewRepositories)
Named Results
Provide multiple implementations:
type CacheResults struct {
godi.Out
Redis Cache `name:"redis"`
Memory Cache `name:"memory"`
Default Cache // Unnamed, available as regular Cache
}
func NewCaches(config *Config) CacheResults {
return CacheResults{
Redis: NewRedisCache(config.RedisURL),
Memory: NewMemoryCache(),
Default: NewRedisCache(config.RedisURL), // Same as Redis
}
}
Group Results
Add multiple services to a group:
type HandlerResults struct {
godi.Out
UserHandler http.Handler `group:"routes"`
OrderHandler http.Handler `group:"routes"`
ProductHandler http.Handler `group:"routes"`
HealthHandler http.Handler `group:"routes"`
}
func NewHandlers(services *Services) HandlerResults {
return HandlerResults{
UserHandler: NewUserHandler(services.UserService),
OrderHandler: NewOrderHandler(services.OrderService),
ProductHandler: NewProductHandler(services.ProductService),
HealthHandler: NewHealthHandler(),
}
}
Advanced Patterns
Nested Parameter Objects
type DatabaseConfig struct {
ConnectionString string
MaxConnections int
}
type CacheConfig struct {
RedisURL string
TTL time.Duration
}
type ServiceConfig struct {
godi.In
Database DatabaseConfig
Cache CacheConfig
Logger Logger
}
func NewService(config ServiceConfig) *Service {
// Use nested configuration
db := connectDB(config.Database.ConnectionString)
cache := connectCache(config.Cache.RedisURL)
return &Service{
db: db,
cache: cache,
logger: config.Logger,
}
}
Combining In and Out
// Input parameters
type ServiceParams struct {
godi.In
DB *sql.DB
Logger Logger
Config *Config
}
// Output results
type ServiceResults struct {
godi.Out
UserService *UserService
OrderService *OrderService `name:"orders"`
AdminService *AdminService `name:"admin"`
PublicHandler http.Handler `group:"public-routes"`
AdminHandler http.Handler `group:"admin-routes"`
}
// Constructor using both
func NewServices(params ServiceParams) ServiceResults {
userSvc := newUserService(params.DB, params.Logger)
orderSvc := newOrderService(params.DB, params.Logger)
adminSvc := newAdminService(params.DB, params.Logger, params.Config)
return ServiceResults{
UserService: userSvc,
OrderService: orderSvc,
AdminService: adminSvc,
PublicHandler: newPublicAPI(userSvc, orderSvc),
AdminHandler: newAdminAPI(adminSvc),
}
}
Factory Pattern with Parameters
type FactoryParams struct {
godi.In
Config *Config
Logger Logger
Providers map[string]Provider `group:"providers"`
}
type FactoryResult struct {
godi.Out
Factory ServiceFactory
DefaultService Service
}
func NewServiceFactory(params FactoryParams) FactoryResult {
factory := &serviceFactory{
config: params.Config,
logger: params.Logger,
providers: params.Providers,
}
// Create default service
defaultService := factory.CreateService("default")
return FactoryResult{
Factory: factory,
DefaultService: defaultService,
}
}
Best Practices
1. When to Use Parameter Objects
Use parameter objects when:
Constructor has more than 3-4 parameters
You need optional dependencies
You need named or grouped dependencies
Parameters are likely to grow over time
// ✅ Good candidate for parameter object
func NewService(db *sql.DB, cache Cache, logger Logger,
emailer EmailService, config *Config, metrics Metrics) *Service
// ✅ Simplified with parameter object
func NewService(params ServiceParams) *Service
2. Naming Conventions
// Input parameter types: add "Params" suffix
type UserServiceParams struct {
godi.In
// ...
}
// Output result types: add "Result" or "Results" suffix
type RepositoryResults struct {
godi.Out
// ...
}
3. Field Documentation
type ServiceParams struct {
godi.In
// DB is the primary database connection (required)
DB *sql.DB
// Logger is used for structured logging (optional)
// If not provided, a default console logger will be used
Logger Logger `optional:"true"`
// Cache is used for performance optimization (optional)
// If not provided, caching will be disabled
Cache Cache `optional:"true"`
// Handlers are HTTP handlers to be registered (group)
// All handlers in the "routes" group will be included
Handlers []http.Handler `group:"routes"`
}
4. Validation
type ServiceParams struct {
godi.In
Config *Config
DB *sql.DB
}
func NewService(params ServiceParams) (*Service, error) {
// Validate required fields
if params.Config == nil {
return nil, errors.New("config is required")
}
if params.Config.APIKey == "" {
return nil, errors.New("API key is required")
}
if params.DB == nil {
return nil, errors.New("database is required")
}
return &Service{
config: params.Config,
db: params.DB,
}, nil
}
5. Avoid Overuse
// ❌ Overkill for simple cases
type LoggerParams struct {
godi.In
Config *Config
}
func NewLogger(params LoggerParams) Logger {
return &logger{level: params.Config.LogLevel}
}
// ✅ Simple is better
func NewLogger(config *Config) Logger {
return &logger{level: config.LogLevel}
}
Testing with Parameter Objects
Mock Specific Fields
func TestServiceWithMocks(t *testing.T) {
collection := godi.NewServiceCollection()
// Register mocks
collection.AddSingleton(func() *sql.DB { return mockDB })
collection.AddSingleton(func() Logger { return mockLogger })
collection.AddSingleton(func() Cache { return nil }) // Optional
// Register service that uses parameter object
collection.AddScoped(NewService)
provider, _ := collection.BuildServiceProvider()
service, _ := godi.Resolve[*Service](provider)
// Test with mocked dependencies
}
Test Helpers
func createTestParams() ServiceParams {
return ServiceParams{
DB: createTestDB(),
Logger: NewTestLogger(),
Cache: NewTestCache(),
}
}
func TestServiceBehavior(t *testing.T) {
params := createTestParams()
// Override specific fields for test
params.Cache = nil // Test without cache
service := NewService(params)
// ... test service behavior
}
Common Patterns
Configuration Parameters
type AppParams struct {
godi.In
// Configuration
Config *Config
Secrets *Secrets `optional:"true"`
FeatureFlags *FeatureFlags `optional:"true"`
// Infrastructure
DB *sql.DB
Cache Cache `optional:"true"`
Queue Queue `optional:"true"`
// Services
Logger Logger
Metrics Metrics `optional:"true"`
Tracer Tracer `optional:"true"`
}
Multi-Database Parameters
type DatabaseParams struct {
godi.In
// Different databases for different purposes
UserDB Database `name:"users"`
OrderDB Database `name:"orders"`
AnalyticsDB Database `name:"analytics" optional:"true"`
// Connection pooling
MaxConns int `optional:"true"`
}
Plugin Parameters
type PluginParams struct {
godi.In
// Core services available to plugins
Logger Logger
Config *Config
EventBus EventBus
// All registered plugins
Plugins []Plugin `group:"plugins"`
}
Summary
Parameter objects provide:
Clean constructors with many dependencies
Optional dependencies with
optional:"true"Named dependencies for multiple implementations
Groups for collections of services
Multiple returns with result objects
Use them to keep your dependency injection code maintainable and extensible as your application grows.