Какие распространённые паттерны применяются для структурирования Go-приложения (например, Clean Architecture, hexagonal)?
Go-приложения структурируют по Clean Architecture (domain → usecase → interface → infrastructure) или Hexagonal (core + ports + adapters), разделяя бизнес-логику от инфраструктуры через интерфейсы.
Архитектурные паттерны Go-приложений
Большинство серьёзных Go-проектов используют один из двух подходов: Clean Architecture или Hexagonal Architecture (она же Ports & Adapters). Оба требуют явного разделения бизнес-логики и инфраструктурного кода через интерфейсы.
Clean Architecture на практике
Типичная структура каталогов:
myapp/
├── cmd/
│ └── api/
│ └── main.go
├── internal/
│ ├── domain/ # сущности, value objects, domain errors
│ │ ├── user.go
│ │ └── order.go
│ ├── usecase/ # бизнес-логика, не знает про HTTP/DB
│ │ ├── user_service.go
│ │ └── interfaces.go # порты (интерфейсы репозиториев)
│ ├── adapter/
│ │ ├── http/ # handlers, middleware
│ │ └── postgres/ # реализации репозиториев
│ └── config/
└── pkg/ # shared utilities
Ключевые слои и их зависимости:
- domain — чистые структуры и методы, нет зависимостей на внешние пакеты
- usecase — бизнес-сценарии, зависит только от
domainи объявляет интерфейсы - adapter — HTTP-хендлеры, Postgres/Redis-репозитории, реализующие интерфейсы из
usecase - cmd — точка входа, wire-up зависимостей (ручной DI или
google/wire)
// internal/usecase/interfaces.go
package usecase
type UserRepository interface {
FindByID(ctx context.Context, id int64) (*domain.User, error)
Save(ctx context.Context, u *domain.User) error
}
// internal/usecase/user_service.go
package usecase
type UserService struct {
repo UserRepository
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
func (s *UserService) GetUser(ctx context.Context, id int64) (*domain.User, error) {
user, err := s.repo.FindByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("GetUser: %w", err)
}
return user, nil
}
// internal/adapter/postgres/user_repo.go
package postgres
type UserRepo struct {
db *pgxpool.Pool
}
func (r *UserRepo) FindByID(ctx context.Context, id int64) (*domain.User, error) {
row := r.db.QueryRow(ctx, `SELECT id, name, email FROM users WHERE id=$1`, id)
var u domain.User
if err := row.Scan(&u.ID, &u.Name, &u.Email); err != nil {
return nil, err
}
return &u, nil
}
Hexagonal Architecture
Отличие от Clean — терминология: порты (интерфейсы) делятся на driving ports (API, CLI) и driven ports (DB, email). Адаптеры реализуют порты. Структурно похоже, но явно выделяются:
ports/inbound/— интерфейсы use-case-ов (для хендлеров)ports/outbound/— интерфейсы инфраструктуры (репозитории, email-клиенты)adapters/primary/— HTTP/gRPC/CLI адаптерыadapters/secondary/— Postgres, Redis, S3 адаптеры
Dependency Injection: ручной vs google/wire
// cmd/api/main.go — ручной wire-up
func main() {
db := postgres.NewPool(cfg.DSN)
userRepo := postgres.NewUserRepo(db)
userSvc := usecase.NewUserService(userRepo)
h := httphandler.NewUserHandler(userSvc)
router := chi.NewRouter()
router.Get("/users/{id}", h.GetUser)
http.ListenAndServe(":8080", router)
}
Для больших проектов — github.com/google/wire генерирует wire-код на основе провайдеров.
Подводные камни
- Циклические импорты: если
domainимпортируетusecase— нарушение правила зависимостей, Go не скомпилирует. - Толстые интерфейсы: Repository с 20 методами — нарушение ISP; дробите на
UserReaderиUserWriter. - Бизнес-логика в хендлерах: проверки и расчёты в HTTP-слое делают код нетестируемым.
- Анемичная domain-модель: структуры только с полями без методов — скрытая потеря преимуществ DDD.
- Слишком глубокая вложенность каталогов: Go не требует Java-подобных иерархий; 3 уровня обычно достаточно.
- Утечка инфраструктурных типов в usecase: передача
*sql.Txилиpgxpool.Poolнапрямую — зависимость от реализации. - Отсутствие context propagation: не передавать
ctxчерез все слои ломает deadline/cancellation. - God-пакет internal/: все в одном пакете без разбивки — антипаттерн, убивает модульность и тестирование.
Common mistakes
- Давать ответ про структурирование Go-приложений только на уровне определения, не показывая поведение в реальном приложении.
- Игнорировать границы ответственности вокруг темы «структурирование Go-приложений»: кто отменяет работу, кто владеет ресурсом и где формируется ответ клиенту.
- Не связывать структурирование Go-приложений с observability, тестированием или безопасностью, когда это влияет на продакшен-поведение.
What the interviewer is testing
- Точно объясняет, что именно делает структурирование Go-приложений и где это используется в Go-коде.
- Связывает структурирование Go-приложений с корректным lifecycle запроса, отменой, конкурентностью или конфигурацией сервера там, где это уместно.
- Не изобретает API и опирается на реальные контракты официальной документации.