Dependency Injection in Go: A Practical Guide

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

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

Blogs

Book Recommendations