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 и опирается на реальные контракты официальной документации.