Dependency Injection in Go: for Beginners — Examples and Principles

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

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 Notifier interface. Now UserController does not know which specific service is being used — Email or SMS.
  • The constructor NewUserController accepts Notifier as 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.

Blogs

Book Recommendations