Introduction: Why Dependency Injection Matters in Go
Go (Golang) is a language that values simplicity and explicitness. However, as a project grows, the need to manage dependencies between components arises. Dependency Injection (DI) is a design pattern that helps create loosely coupled, testable, and easily maintainable code. Unlike frameworks with reflection magic (like Spring in Java), Go offers a more pragmatic approach: passing dependencies through interfaces and constructors.
In this article, we will break down how to implement DI in Go without third-party libraries, using pure interfaces, and also look at popular DI containers. You will learn how dependency injection helps write unit tests and follow SOLID principles.
Basics: Interfaces and Constructor Injection
Go doesn't have classes, but it has interfaces — these are behavioral contracts. They are the foundation for DI. Instead of creating a concrete implementation inside a struct, we pass it from the outside.
Example without DI (Tight Coupling)
package main
import "fmt"
// Logger — concrete implementationtype Logger struct{}
func (l *Logger) Log(msg string) { fmt.Println("LOG:", msg)}
// Service — tightly depends on Loggertype Service struct { logger *Logger}
func NewService() *Service { return &Service{logger: &Logger{}}}
func (s *Service) DoWork() { s.logger.Log("Work completed")}
func main() { svc := NewService() svc.DoWork()}Problem: Service cannot be tested without creating a real Logger. It's impossible to replace it with a mock or another logger.
Example with DI via Interface
package main
import "fmt"
// LogInterface — abstraction for loggingtype LogInterface interface { Log(msg string)}
// ConsoleLogger — implementationtype ConsoleLogger struct{}
func (l *ConsoleLogger) Log(msg string) { fmt.Println("LOG:", msg)}
// Service — now depends on the interfacetype Service struct { logger LogInterface}
// NewService — constructor with dependency injectionfunc NewService(logger LogInterface) *Service { return &Service{logger: logger}}
func (s *Service) DoWork() { s.logger.Log("Work completed")}
func main() { logger := &ConsoleLogger{} svc := NewService(logger) svc.DoWork()}Now Service works with any object that implements LogInterface. This simplifies testing: we can pass a mock.
Advanced Technique: DI Containers in Go
When a project grows, manually passing dependencies becomes tedious. DI containers come to the rescue — libraries that automatically resolve the dependency graph. In Go, google/wire (from Google) and uber-go/dig (from Uber) are popular.
Example with google/wire
Wire uses code generation, which eliminates reflection at runtime — this is in the spirit of Go.
// wire.go//go:build wireinject// +build wireinject
package main
import "github.com/google/wire"
func InitializeService() *Service { wire.Build(NewService, NewConsoleLogger) return nil}After running wire, it will generate code that creates a ConsoleLogger and passes it to NewService.
Example with uber-go/dig
Dig works via reflection but is very convenient for dynamic assembly.
package main
import ( "go.uber.org/dig")
func main() { container := dig.New()
// Register constructors container.Provide(NewConsoleLogger) container.Provide(NewService)
// Invoke a function that uses the depend