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
Не все зависимости нужно внедрять. Утилитарные функции, константы, настройки (если они не меняются в тестах) можно оставить внутри модуля.
Блоги
Рекомендаций по книгам
Читайте также