EchoSeniorSystem design

Как структурировать большое приложение на Echo с использованием dependency injection?

Большое Echo-приложение структурируют через явную инъекцию зависимостей: контейнер зависимостей передаётся в хендлеры через метод-получатель на struct, а не через глобальные переменные или init().

Структура большого приложения на Echo с DI

Go не имеет встроенного IoC-контейнера, поэтому зависимости передаются явно — через конструкторы и методы. Наиболее распространённый паттерн: handler struct хранит ссылки на сервисы, а main.go (или пакет app) собирает граф зависимостей вручную.

Типичная структура каталогов

myapp/
  cmd/
    server/
      main.go          // точка входа, wire-up
  internal/
    config/
      config.go        // загрузка конфига (envconfig / viper)
    db/
      postgres.go      // *pgxpool.Pool
    repository/
      user_repo.go     // интерфейс + pg-реализация
    service/
      user_service.go  // бизнес-логика
    handler/
      user_handler.go  // echo.HandlerFunc через метод struct
    router/
      router.go        // регистрация маршрутов
  go.mod

Пример реализации

// internal/repository/user_repo.go
package repository

import "context"

type User struct {
	ID   int64  `json:"id"`
	Name string `json:"name"`
}

type UserRepository interface {
	FindByID(ctx context.Context, id int64) (*User, error)
}
// internal/service/user_service.go
package service

import (
	"context"

	"myapp/internal/repository"
)

type UserService struct {
	repo repository.UserRepository
}

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

func (s *UserService) GetUser(ctx context.Context, id int64) (*repository.User, error) {
	return s.repo.FindByID(ctx, id)
}
// internal/handler/user_handler.go
package handler

import (
	"net/http"
	"strconv"

	"github.com/labstack/echo/v4"
	"myapp/internal/service"
)

type UserHandler struct {
	svc *service.UserService
}

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

func (h *UserHandler) GetUser(c echo.Context) error {
	id, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "invalid id")
	}
	user, err := h.svc.GetUser(c.Request().Context(), id)
	if err != nil {
		return err
	}
	return c.JSON(http.StatusOK, user)
}
// cmd/server/main.go
package main

import (
	"log"

	"github.com/labstack/echo/v4"
	"myapp/internal/db"
	"myapp/internal/handler"
	"myapp/internal/repository"
	"myapp/internal/service"
)

func main() {
	pool, err := db.NewPool("postgres://user:pass@localhost:5432/myapp")
	if err != nil {
		log.Fatal(err)
	}

	// Граф зависимостей
	userRepo := repository.NewPostgresUserRepo(pool)
	userSvc := service.NewUserService(userRepo)
	userHandler := handler.NewUserHandler(userSvc)

	e := echo.New()
	e.GET("/users/:id", userHandler.GetUser)

	log.Fatal(e.Start(":8080"))
}

Автоматическая инъекция через Wire или fx

При большом числе зависимостей ручной wire-up становится громоздким. Используйте google/wire (кодогенерация) или go.uber.org/fx (runtime DI):

// wire.go (для google/wire)
//go:build wireinject

package main

import (
	"github.com/google/wire"
	"myapp/internal/handler"
	"myapp/internal/repository"
	"myapp/internal/service"
)

func initUserHandler(dsn string) (*handler.UserHandler, error) {
	wire.Build(
		db.NewPool,
		repository.NewPostgresUserRepo,
		service.NewUserService,
		handler.NewUserHandler,
	)
	return nil, nil
}

Подводные камни

  • Глобальные переменные вместо DI — делают тестирование невозможным без мокирования пакет-уровневых состояний.
  • Передача *echo.Echo в сервисный слой — сервис не должен знать о транспорте; только хендлеры работают с echo.Context.
  • Инициализация БД внутри init() — порядок вызовов init непредсказуем при импорте.
  • Отсутствие интерфейсов у репозиториев — без интерфейса нельзя подменить реализацию в тестах.
  • Утечка *pgxpool.Pool через несколько слоёв вместо передачи через репозиторий — нарушает инкапсуляцию.
  • Смешение конфигурации и логики в одном struct — выделите config.Config отдельно и передавайте только нужные поля.
  • При использовании fx: забытый fx.Annotate при нескольких реализациях одного интерфейса приводит к панике при старте.
  • Circular dependencies — признак неправильного разбиения на пакеты; решается введением дополнительного слоя абстракции.

Common mistakes

  • Давать ответ про структурирование большого приложения Echo с DI только на уровне определения, не показывая поведение в реальном приложении.
  • Игнорировать границы ответственности вокруг темы «структурирование большого приложения Echo с DI»: кто отменяет работу, кто владеет ресурсом и где формируется ответ клиенту.
  • Не связывать структурирование большого приложения Echo с DI с observability, тестированием или безопасностью, когда это влияет на продакшен-поведение.

What the interviewer is testing

  • Точно объясняет, что именно делает структурирование большого приложения Echo с DI и где это используется в Go-коде.
  • Связывает структурирование большого приложения Echo с DI с корректным lifecycle запроса, отменой, конкурентностью или конфигурацией сервера там, где это уместно.
  • Не изобретает API и опирается на реальные контракты официальной документации.

Sources

Related topics