← Назад к вопросам

Как знаешь способы делать Dependency Injection?

2.0 Middle🔥 161 комментариев
#Микросервисы и архитектура

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Способы Dependency Injection в Go

В Go dependency injection (DI) реализуется несколько иначе, чем в классических ООП-языках, из-за отсутствия встроенных конструкторов и ограниченной поддержки наследования. Однако существует несколько эффективных подходов.

Constructor Injection (Внедрение через конструктор)

Самый распространенный и рекомендуемый способ. Зависимости передаются через параметры конструктора (обычно функции New*).

package user

type Service struct {
    repo Repository
    validator *Validator
    logger *log.Logger
}

// NewService - конструктор с явным внедрением зависимостей
func NewService(repo Repository, validator *Validator, logger *log.Logger) *Service {
    return &Service{
        repo:      repo,
        validator: validator,
        logger:    logger,
    }
}

func (s *Service) CreateUser(ctx context.Context, user User) error {
    if err := s.validator.Validate(user); err != nil {
        s.logger.Printf("validation failed: %v", err)
        return err
    }
    return s.repo.Save(ctx, user)
}

Преимущества:

  • Явные зависимости - сразу видно, от чего зависит тип
  • Неизменяемость после создания
  • Легко тестировать с моками
  • Компилятор проверяет полноту инициализации

Setter Injection (Внедрение через сеттеры)

Менее распространен в Go, но иногда используется для опциональных зависимостей или конфигурации.

type ConfigurableService struct {
    cache Cache
}

// SetCache позволяет установить кэш опционально
func (s *ConfigurableService) SetCache(cache Cache) {
    s.cache = cache
}

// Использование
service := &ConfigurableService{}
if useCache {
    service.SetCache(redisCache)
}

Недостатки: объект может быть в невалидном состоянии до вызова сеттеров.

Interface Injection

Внедрение через интерфейсы, где зависимость реализует метод для внедрения самой себя.

type LoggerInjector interface {
    InjectLogger(logger *log.Logger)
}

type Service struct {
    logger *log.Logger
}

func (s *Service) InjectLogger(logger *log.Logger) {
    s.logger = logger
}

Этот подход редко используется в чистом виде в Go, но вариации применяются в некоторых DI-фреймворках.

Functional Options Pattern

Популярный идиоматический способ для конфигурации с множеством опциональных параметров.

type Server struct {
    addr    string
    timeout time.Duration
    logger  *log.Logger
}

type Option func(*Server)

func WithTimeout(timeout time.Duration) Option {
    return func(s *Server) {
        s.timeout = timeout
    }
}

func WithLogger(logger *log.Logger) Option {
    return func(s *Server) {
        s.logger = logger
    }
}

func NewServer(addr string, opts ...Option) *Server {
    s := &Server{
        addr:    addr,
        timeout: defaultTimeout,
        logger:  defaultLogger,
    }
    
    for _, opt := range opts {
        opt(s)
    }
    return s
}

// Использование
server := NewServer(":8080",
    WithTimeout(30*time.Second),
    WithLogger(customLogger),
)

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

DI Контейнеры и Фреймворки

Для сложных приложений с множеством зависимостей:

  1. google/wire - compile-time DI от Google

    // wire.go
    // +build wireinject
    
    func InitializeServer() (*Server, error) {
        wire.Build(
            NewServer,
            NewDatabase,
            NewConfig,
        )
        return &Server{}, nil
    }
    
  2. uber-go/dig - runtime DI контейнер от Uber

    container := dig.New()
    container.Provide(NewConfig)
    container.Provide(NewDatabase)
    container.Provide(NewServer)
    
    container.Invoke(func(s *Server) {
        s.Run()
    })
    

Manual DI (Ручная сборка)

Самый простой и прозрачный подход для небольших приложений:

func main() {
    // Сборка зависимостей в точке входа
    logger := log.New(os.Stdout, "APP: ", log.LstdFlags)
    config := LoadConfig()
    db := NewDatabase(config.DB)
    repo := NewUserRepository(db)
    service := NewUserService(repo, logger)
    
    server := NewServer(config.Port, service, logger)
    server.Run()
}

Практические рекомендации

  1. Предпочитайте constructor injection для обязательных зависимостей
  2. Используйте интерфейсы для абстракции зависимостей:
    type UserRepository interface {
        Save(ctx context.Context, user User) error
        FindByID(ctx context.Context, id string) (*User, error)
    }
    
  3. Избегайте глобальных состояний и синглтонов как антипаттернов DI
  4. Тестируемость - главный критерий выбора подхода:
    func TestUserService(t *testing.T) {
        mockRepo := new(MockRepository)
        logger := log.New(io.Discard, "", 0)
        service := NewUserService(mockRepo, logger)
        // тестирование...
    }
    
  5. Чистая архитектура - внедряйте зависимости от абстракций, а не от реализаций

Выбор подхода

  • Малые проекты: ручная DI или functional options
  • Средние проекты: constructor injection + интерфейсы
  • Крупные проекты: DI фреймворки (wire для compile-time безопасности, dig для динамических сценариев)

В Go сообщество предпочитает явные, compile-time безопасные подходы, поэтому constructor injection и functional options стали де-факто стандартом. DI контейнеры используются реже, чем в Java/C#, но имеют свою нишу в enterprise-приложениях. Главное - сохранять простоту и явность зависимостей, избегая магического разрешения зависимостей в runtime.