Dependency Injection в Go: практическое руководство

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Dependency Injection в Go: от теории к реальному коду

Dependency Injection (DI) — это один из ключевых паттернов проектирования, который помогает создавать слабосвязанный, тестируемый и поддерживаемый код. В Go, с его философией простоты и явности, внедрение зависимостей может быть реализовано как вручную, так и с помощью библиотек. В этой статье мы разберем, как правильно применять DI в Golang на практике, рассмотрим ручной подход, а также популярную библиотеку google/wire.

1. Зачем нужен Dependency Injection в Go?

Представьте, что вы пишете HTTP-сервис для работы с пользователями. Без DI ваш код может выглядеть так:

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) {
    // работа с h.db
}

Проблемы очевидны:

  • Жесткая связь — хендлер сам создает базу данных.
  • Сложность тестирования — невозможно подменить db на мок.
  • Нарушение Single Responsibility — хендлер отвечает и за создание зависимостей, и за бизнес-логику.

DI решает эти проблемы, передавая зависимости извне:

type UserHandler struct {
    db Database
}

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

// Теперь можно легко подменить на мок
type Database interface {
    GetUser(id int) (*User, error)
}

2. Ручное внедрение зависимостей: простота и контроль

Go не требует фреймворков для DI. Самый простой и прозрачный способ — передавать зависимости через конструкторы. Рассмотрим пример с тремя уровнями приложения: handler → service → repository.

// repository/user.go
type 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) {
    // реализация
}
// service/user.go
type 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.go
type UserHandler struct {
    svc *UserService
}

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

func (h *UserHandler) HandleGetUser(w http.ResponseWriter, r *http.Request) {
    // работа с h.svc
}

Теперь сборка приложения происходит в 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))
}

Плюсы ручного DI:

  • Полная прозрачность — вы видите все зависимости явно.
  • Нет магии и лишних зависимостей в проекте.
  • Идеально для небольших и средних проектов.

Минусы: при росте приложения (50+ компонентов) ручная сборка становится громоздкой и подвержена ошибкам.

3. Автоматизация с google/wire

Библиотека github.com/google/wire генерирует код для сборки зависимостей. Она не является runtime-контейнером — это кодогенератор, что полностью соответствует философии Go.

Установка:

go get github.com/google/wire
go install github.com/google/wire/cmd/wire@latest

Пример использования:

// wire.go
//go:build wireinject
// +build wireinject

package main

import "github.com/google/wire"

func InitializeUserHandler(db *sql.DB) *UserHandler {
    wire.Build(
        NewPostgresUserRepo,
        NewUserService,
        NewUserHandler,
    )
    return nil // заглушка
}

Затем запускаем генерацию:

wire

Wire создаст файл wire_gen.go:

// wire_gen.go
// Code generated by Wire. DO NOT EDIT.

func InitializeUserHandler(db *sql.DB) *UserHandler {
    postgresUserRepo := NewPostgresUserRepo(db)
    userService := NewUserService(postgresUserRepo)
    userHandler := NewUserHandler(userService)
    return userHandler
}

Теперь main.go становится лаконичным:

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

Когда стоит использовать wire:

  • Проект имеет 10+ компонентов с глубокой вложенностью.
  • Нужно часто менять реализации (например, переключение между БД).
  • Команда разработчиков разного уровня — wire упрощает понимание зависимостей.

4. Практические советы и анти-паттерны

1. Избегайте DI-контейнеров в runtime

Библиотеки вроде dig или fx от Uber — мощные, но они вносят магию и рефлексию. Для 95% проектов Go достаточно ручного DI или wire.

2. Используйте интерфейсы для абстракции

Интерфейсы в Go — идеальный инструмент для DI. Определяйте их на стороне потребителя, а не поставщика:

// service/user.go (потребитель)
type UserStorage interface {
    FindByID(id int) (*User, error)
}

type UserService struct {
    storage UserStorage
}

3. Не злоупотребляйте DI

Не все зависимости нужно внедрять. Утилитарные функции, константы, настройки (если они не меняются в тестах) можно оставить внутри модуля.

Блоги

Рекомендаций по книгам