Dependency Injection in Go: for Beginners
Dependency Injection (DI) is one of the key design patterns that helps write clean, testable, and maintainable code. In the world of Go, where simplicity and clarity are valued above all, DI helps avoid creating "god objects" and tight coupling between components. In this article, we will break down what Dependency Injection in Go is, why it is needed, and how to apply it in practice using simple and clear examples.
What is Dependency Injection?
Imagine you are writing a program that sends welcome emails to users. Your code would likely look like this:
package main
import "fmt"
type EmailService struct{}
func (e *EmailService) SendWelcomeEmail(userEmail string) { fmt.Printf("Sending email to %s", userEmail)}
type UserController struct { emailService *EmailService}
func NewUserController() *UserController { return &UserController{ emailService: &EmailService{}, // Tight coupling }}
func (uc *UserController) RegisterUser(email string) { // ... registration logic uc.emailService.SendWelcomeEmail(email)}
func main() { controller := NewUserController() controller.RegisterUser("user@example.com")}The problem with this code is that UserController creates an instance of EmailService itself. This is called tight coupling. If you want to replace EmailService with, say, SmsService or use a mock object for testing, you will have to change the code inside UserController.
Dependency Injection solves this problem. Instead of creating dependencies inside the struct, we "inject" them from the outside. The main idea is: "Don't ask for what you need. Say what you need and get it ready-made."
Manual Dependency Injection via Constructor
The simplest and most common way of DI in Go is passing dependencies through a constructor function. Let's rewrite our example:
package main
import "fmt"
// Define an interface for flexibilitytype Notifier interface { Send(to string, message string) error}
type EmailService struct{}
func (e *EmailService) Send(to string, message string) error { fmt.Printf("Email to %s: %s", to, message) return nil}
type SmsService struct{}
func (s *SmsService) Send(to string, message string) error { fmt.Printf("SMS to %s: %s", to, message) return nil}
type UserController struct { notifier Notifier // Dependency via interface}
// NewUserController — constructor that accepts a dependencyfunc NewUserController(n Notifier) *UserController { return &UserController{ notifier: n, }}
func (uc *UserController) RegisterUser(email string) { // ... business logic uc.notifier.Send(email, "Welcome!")}
func main() { // Manually inject the dependency emailNotifier := &EmailService{} controller := NewUserController(emailNotifier) controller.RegisterUser("user@example.com")
// Easily change the implementation smsNotifier := &SmsService{} controller2 := NewUserController(smsNotifier) controller2.RegisterUser("+123456789")}What changed?
- We defined the
Notifierinterface. NowUserControllerdoes not know which specific service is being used — Email or SMS. - The constructor
NewUserControlleracceptsNotifieras an argument. This is dependency injection. - In
main, we decide which implementation to pass.
This approach makes the code flexible, testable, and compliant with the Inversion of Control principle.