Introduction
Dependency injection (DI) is the practice of passing dependencies into a function or struct rather than creating them internally. Go favors explicit DI through constructors—no magic frameworks needed. This pattern is the foundation of testable, modular Go applications.
Key Concepts
- Constructor Injection: Passing dependencies as parameters to a
New*function that returns a struct. - Wiring in main(): The
mainfunction creates all real dependencies and injects them into services. - No Global State: Dependencies are struct fields, not package-level variables.
Real World Context
Your UserService needs a database, a cache, and a logger. If it creates these internally, you cannot test it without a running database. Constructor injection lets you pass mocks in tests and real implementations in production.
Deep Dive
The constructor pattern accepts interfaces and returns a concrete struct:
gotype UserService struct { db Database cache Cache logger Logger } func NewUserService(db Database, cache Cache, logger Logger) *UserService { return &UserService{ db: db, cache: cache, logger: logger, } }
All wiring happens in main():
gofunc main() { // Create real dependencies db := database.New(os.Getenv("DB_URL")) cache := redis.New(os.Getenv("REDIS_URL")) logger := slog.Default() // Inject into services userSvc := service.NewUserService(db, cache, logger) orderSvc := service.NewOrderService(db, logger) // Create server with services srv := server.New(userSvc, orderSvc) srv.Run() }
This approach makes the dependency graph explicit. You can trace every dependency by reading main(). There is no hidden state, no service locator, and no runtime reflection.
Common Pitfalls
- Using package-level variables for dependencies — Global state makes testing impossible and creates hidden coupling. Always inject through constructors.
- Using a DI framework — Go's simplicity means you rarely need frameworks like Wire or Dig. Manual DI in main() is usually clearer.
Best Practices
- Accept interfaces, store as struct fields — This enables mocking in tests and swapping implementations.
- Keep main() as the composition root — All dependency creation and wiring should happen in main or a dedicated setup function.
Summary
- Pass dependencies through constructors, not global state.
- Wire everything in main() for explicit dependency graphs.
- Accept interfaces in constructors for testability.
- Manual DI is usually clearer than framework-based DI in Go.
Code Examples
// Application struct wires all dependencies together.
// This is the composition root of the application.
type Application struct {
config *Config
db *sql.DB
cache *redis.Client
server *http.Server
}
func NewApplication(cfg *Config) (*Application, error) {
db, err := sql.Open("postgres", cfg.DatabaseURL)
if err != nil {
return nil, fmt.Errorf("opening database: %w", err)
}
return &Application{config: cfg, db: db}, nil
}