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

Dependency Injection in Go: From Theory to Real Code



Dependency Injection (DI) is one of the key design patterns that helps create loosely coupled, testable, and maintainable code. In Go, with its philosophy of simplicity and explicitness, dependency injection can be implemented both manually and using libraries. In this article, we will explore how to properly apply DI in Golang in practice, examine the manual approach, and also the popular library google/wire.



1. Why is Dependency Injection Needed in Go?



Imagine you are writing an HTTP service for working with users. Without DI, your code might look like this:



package main

import "database/sql"

type UserHandler struct { db *sql.DB}

func NewUserHandler() *UserHandler { db, _ := sql.Open("postgres", "conn_string") return &UserHandler{db: db}}

func (h *UserHandler) GetUser(id int) (*User, error) { // working with h.db}


The problems are obvious:

  • Tight coupling — the handler creates the database itself.
  • Testing difficulty — it's impossible to replace db with a mock.
  • Violation of Single Responsibility — the handler is responsible both for creating dependencies and for business logic.


DI solves these problems by passing dependencies from the outside:



type UserHandler struct {    db Database}

func NewUserHandler(db Database) *UserHandler { return &UserHandler{db: db}}

// Now it's easy to replace with a mocktype Database interface { GetUser(id int) (*User, error)}


2. Manual Dependency Injection: Simplicity and Control



Go does not require frameworks for DI. The simplest and most transparent way is to pass dependencies through constructors. Let's consider an example with three application layers: handler → service → repository.



// repository/user.gotype UserRepository interface {    FindByID(id int) (*User, error)}

type postgresUserRepo struct { db *sql.DB}

func NewPostgresUserRepo(db *sql.DB) *postgresUserRepo { return &postgresUserRepo{db: db}}

func (r *postgresUserRepo) FindByID(id int) (*User, error) { // implementation}


// service/user.gotype UserService struct {    repo UserRepository}

func NewUserService(repo UserRepository) *UserService { return &UserService{repo: repo}}

func (s *UserService) GetUser(id int) (*User, error) { return s.repo.FindByID(id)}


// handler/user.gotype UserHandler struct {    svc *UserService}

func NewUserHandler(svc *UserService) *UserHandler { return &UserHandler{svc: svc}}

func (h *UserHandler) HandleGetUser(w http.ResponseWriter, r *http.Request) { // working with h.svc}


Now the application assembly happens in main.go:



func main() {    db, _ := sql.Open("postgres", os.Getenv("DB_URL"))        repo := NewPostgresUserRepo(db)    svc := NewUserService(repo)    handler := NewUserHandler(svc)        http.HandleFunc("/user", handler.HandleGetUser)    log.Fatal(http.ListenAndServe(":8080", nil))}


Pros of manual DI:

  • Full transparency — you see all dependencies explicitly.
  • No magic or unnecessary dependencies in the project.
  • Ideal for small and medium-sized projects.


Cons: as the application grows (50+ components), manual assembly becomes cumbersome and error-prone.



3. Automation with google/wire



The library github.com/google/wire generates code for dependency assembly. It is not a runtime container — it is a code generator, which fully aligns with the Go philosophy.



Installation:

go get github.com/google/wir                

Blogs

Book Recommendations