GoSeniorSystem design

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

Sources

Related topics