Dependency Injection в Go: для начинающих — примеры и принципы

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

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

Начать курс

Dependency Injection в Go: для начинающих

Dependency Injection (DI), или внедрение зависимостей, — это один из ключевых паттернов проектирования, который помогает писать чистый, тестируемый и поддерживаемый код. В мире Go, где простота и ясность ценятся превыше всего, DI позволяет избежать создания «божественных объектов» и жестких связей между компонентами. В этой статье мы разберем, что такое Dependency Injection в Go, зачем он нужен, и как его применять на практике, используя простые и понятные примеры.

Что такое Dependency Injection?

Представьте, что вы пишете программу, которая отправляет приветственные письма пользователям. Ваш код, скорее всего, будет выглядеть так:

package main

import "fmt"

type EmailService struct{}

func (e *EmailService) SendWelcomeEmail(userEmail string) {
    fmt.Printf("Отправка письма на %s\n", userEmail)
}

type UserController struct {
    emailService *EmailService
}

func NewUserController() *UserController {
    return &UserController{
        emailService: &EmailService{}, // Жесткая связь
    }
}

func (uc *UserController) RegisterUser(email string) {
    // ... логика регистрации
    uc.emailService.SendWelcomeEmail(email)
}

func main() {
    controller := NewUserController()
    controller.RegisterUser("user@example.com")
}

Проблема этого кода в том, что UserController сам создает экземпляр EmailService. Это называется жесткой связью. Если вы захотите заменить EmailService на, скажем, SmsService или использовать мок-объект для тестирования, вам придется менять код внутри UserController.

Dependency Injection решает эту проблему. Вместо того чтобы создавать зависимости внутри структуры, мы «внедряем» их извне. Основная идея: «Не спрашивай, что тебе нужно. Скажи, что тебе нужно, и получи это готовым».

Ручное внедрение зависимостей через конструктор

Самый простой и распространенный способ DI в Go — это передача зависимостей через функцию-конструктор. Давайте перепишем наш пример:

package main

import "fmt"

// Определяем интерфейс для гибкости
type 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\n", to, message)
    return nil
}

type SmsService struct{}

func (s *SmsService) Send(to string, message string) error {
    fmt.Printf("SMS to %s: %s\n", to, message)
    return nil
}

type UserController struct {
    notifier Notifier // Зависимость через интерфейс
}

// NewUserController — конструктор, который принимает зависимость
func NewUserController(n Notifier) *UserController {
    return &UserController{
        notifier: n,
    }
}

func (uc *UserController) RegisterUser(email string) {
    // ... бизнес-логика
    uc.notifier.Send(email, "Добро пожаловать!")
}

func main() {
    // Внедряем зависимость вручную
    emailNotifier := &EmailService{}
    controller := NewUserController(emailNotifier)
    controller.RegisterUser("user@example.com")

    // Легко меняем реализацию
    smsNotifier := &SmsService{}
    controller2 := NewUserController(smsNotifier)
    controller2.RegisterUser("+123456789")
}

Что изменилось?

  • Мы определили интерфейс Notifier. Теперь UserController не знает, какой именно сервис используется — Email или SMS.
  • Конструктор NewUserController принимает Notifier как аргумент. Это и есть внедрение зависимости.
  • В main мы сами решаем, какую реализацию передать.

Этот подход делает код гибким, тестируемым и соответствующим принципу Inversion of Control (IoC).

Почему это важно для тестирования?

Одно из главных преимуществ DI — это возможность легко писать unit-тесты. Вместо реального сервиса мы можем внедрить мок-объект (mock).

// mock_test.go
package main

import "testing"

type MockNotifier struct {
    called bool
    lastTo string
}

func (m *MockNotifier) Send(to string, message string) error {
    m.called = true
    m.lastTo = to
    return nil
}

func TestRegisterUser(t *testing.T) {
    mock := &MockNotifier{}
    controller := NewUserController(mock)

    controller.RegisterUser("test@test.com")

    if !mock.called {
        t.Error("Ожидался вызов Send")
    }
    if mock.lastTo != "test@test.com" {
        t.Errorf("Ожидался email test@test.com, получен %s", mock.lastTo)
    }
}

Без DI нам пришлось бы менять глобальные переменные или использовать сложные хаки. С DI тестирование становится простым и элегантным.

Продвинутые техники: функциональные опции и DI-контейнеры

Для больших проектов ручное внедрение всех зависимостей может стать утомительным. Go предлагает несколько паттернов для упрощения этой задачи.

Паттерн «Функциональные опции»

Этот паттерн полезен, когда у структуры много зависимостей, некоторые из которых могут быть опциональными:

type Config struct {
    Logger     *log.Logger
    DB         *sql.DB
    Notifier   Notifier
    MaxRetries int
}

type Service struct {
    config Config
}

type ServiceOption func(*Config)

func WithLogger(l *log.Logger) ServiceOption {
    return func(c *Config) {
        c.Logger = l
    }
}

func WithNotifier(n Notifier) ServiceOption {
    return func(c *Config) {
        c.Notifier = n
    }
}

func NewService(opts ...ServiceOption) *Service {
    config := &Config{
        MaxRetries: 3, // Значение по умолчанию
    }
    for _, opt := range opts {
        opt(config)
    }
    return &Service{config: *config}
}

DI-контейнеры

В экосистеме Go существуют библиотеки для автоматического управления зависимостями, такие как google/wire (от Google) или uber-go/dig. Они автоматически создают граф зависимостей и инжектируют их. Однако для начинающих рекомендуется сначала освоить ручное внедрение, так как оно дает полный контроль и лучшее понимание происходящего.

// Пример с wire (только для демонстрации концепции)
// +build wireinject

package main

import "github.com/google/wire"

func InitializeController() *UserController {
    wire.Build(NewUserController, NewEmailService)
    return nil
}

Заключение

Блоги

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