Структура Go проектов: от простого к сложному

Как пользоваться руководством

Если вы новичок в Go, не пытайтесь освоить всё сразу. Начните с основ:

Сначала прочитайте:

Когда начнёте писать код:

Остальное изучайте по мере необходимости - не нужно запоминать все паттерны заранее.


Введение

Структура проекта - это одна из тех вещей, которые кажутся простыми на первый взгляд, но становятся источником головной боли по мере роста вашего приложения. В отличие от многих языков, Go предоставляет большую свободу в организации кода, что может быть как благословением, так и проклятием. Это руководство поможет вам понять, как правильно организовать Go проект, избежать типичных ошибок и создать структуру, которая будет легко поддерживаться в долгосрочной перспективе.

Почему структура проекта важна?

Проблемы неорганизованного кода

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

my-project/
├── main.go                    // 800 строк кода
├── stuff.go                   // Разные функции
├── helpers.go                 // Еще больше разных функций
├── database_things.go         // Работа с базой данных
├── api_handlers.go           // HTTP обработчики
├── models.go                 // Структуры данных
├── utils.go                  // "Полезные" функции
└── other_stuff.go            // Непонятно что

Что не так с этой структурой:

  • Отсутствие логической группировки: Код, связанный с одной функциональностью, разбросан по разным файлам
  • Неясные границы ответственности: Нельзя точно сказать, какая именно логика реализована в данном файле
  • Сложность навигации: Найти нужный код становится квестом
  • Проблемы с тестированием: Трудно писать изолированные тесты
  • Конфликты при работе в команде: Разработчики постоянно модифицируют одни и те же файлы

Преимущества хорошей структуры

Правильно организованный проект дает следующие преимущества:

  • Быстрое понимание архитектуры: Новые разработчики могут быстро разобраться в проекте
  • Легкость поддержки: Изменения в одной области не влияют на другие
  • Упрощенное тестирование: Каждый пакет можно тестировать независимо
  • Масштабируемость: Проект может расти без потери управляемости
  • Переиспользование кода: Четкие границы пакетов упрощают повторное использование

Эволюция структуры проекта

Этап 1: Простой исполняемый файл

Для небольших проектов и прототипов начните максимально просто:

calculator/
├── go.mod
├── main.go
└── README.md

Когда использовать: CLI утилиты, простые скрипты, прототипы

Пример содержимого:

// main.go package main import "fmt" func main() { result := add(5, 3) fmt.Printf("Результат: %d\n", result) } func add(a, b int) int { return a + b }

Этап 2: Несколько файлов, один пакет

Когда main.go становится слишком большим, разделите код по файлам:

calculator/
├── go.mod
├── main.go           // package main, точка входа
├── operations.go     // package main, математические операции
├── input.go          // package main, ввод данных
└── output.go         // package main, вывод результатов

Когда использовать: Проекты до 1000 строк кода

Этап 3: Введение внутренних пакетов

Когда различные части кода имеют четкие границы ответственности:

calculator/
├── go.mod
├── main.go
├── internal/
│   ├── calculator/
│   │   ├── operations.go
│   │   └── operations_test.go
│   ├── parser/
│   │   ├── parser.go
│   │   └── parser_test.go
│   └── formatter/
│       ├── formatter.go
│       └── formatter_test.go
└── README.md

Когда использовать: Проекты с четкими функциональными областями

Ключевые директории и их назначение

/cmd - Исполняемые файлы

Директория /cmd содержит точки входа для ваших приложений. Каждое приложение должно иметь свою поддиректорию.

project/
├── cmd/
│   ├── server/           // Веб-сервер
│   │   └── main.go
│   ├── worker/           // Фоновые задачи
│   │   └── main.go
│   └── migration/        // Миграции базы данных
│       └── main.go
└── ...

Мотивация и философия /cmd

Зачем нужна директория /cmd?

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

// Проблема: что делает этот проект?
my-project/
├── main.go              // Исполняемый файл?
├── server.go            // Или это библиотека?
├── client.go            // А это что?
└── utils.go

Историческая проблема: В ранних Go проектах не было четкого разделения между "приложением" и "библиотекой". Когда кто-то делал go get github.com/username/project, неясно было что получится - исполняемый файл или импортируемый пакет.

Решение через /cmd: Четкое разделение намерений

project/
├── cmd/                 // "Это приложения"
│   ├── server/          // Конкретное приложение
│   └── cli/             // Другое приложение
├── internal/            // "Это внутренняя логика"
└── pkg/                 // "Это публичная библиотека"

Почему именно cmd? Название произошло от Unix-традиции, где исполняемые команды хранятся в директориях /bin, /usr/bin, /usr/local/bin. В контексте Go проекта cmd означает "commands" - команды, которые можно выполнить.

Принципы использования:

  • Имя директории = имя исполняемого файла
  • Минимальный код в main.go (импорт и вызов)
  • Не используйте /cmd для проектов с одним исполняемым файлом

Когда НЕ нужна /cmd: Если у вас один исполняемый файл и нет планов на библиотеки, main.go в корне проекта - правильный выбор.

Пример структуры main.go:

// cmd/server/main.go package main import ( "log" "github.com/myorg/myproject/internal/server" ) func main() { if err := server.Run(); err != nil { log.Fatal(err) } }

/internal - Приватные пакеты

Важно: /internal - это не просто соглашение, а встроенная особенность Go. Код в /internal может импортироваться только пакетами из того же репозитория.

project/
├── internal/
│   ├── auth/             // Аутентификация
│   │   ├── auth.go
│   │   ├── jwt.go
│   │   └── auth_test.go
│   ├── database/         // Работа с БД
│   │   ├── postgres.go
│   │   └── migrations.go
│   └── api/              // HTTP API
│       ├── handlers.go
│       ├── middleware.go
│       └── routes.go
└── ...

Мотивация и философия /internal

Почему Go команда создала механизм internal?

До появления /internal в Go 1.4, все пакеты в проекте были публичными. Это создавало серьезные проблемы:

// Проблема: все было публично доступно github.com/company/project/ ├── main.go ├── auth.go // Любой может импортировать! ├── secret_algo.go // Секретный алгоритм тоже публичен! └── database_helpers.go // Внутренние хелперы доступны всем!

Реальная проблема в production: Разработчики других проектов начинали использовать ваши внутренние функции, думая что это стабильный API. Когда вы меняли внутреннюю логику - ломались чужие проекты.

Пример проблемы:

// Ваш проект v1.0 package auth func validateToken(token string) bool { ... } // Внутренняя функция // Кто-то в другом проекте: import "github.com/yourcompany/project/auth" // Использует вашу внутреннюю функцию if auth.validateToken(userToken) { ... } // Вы в v1.1 переименовываете функцию -> чужой код ломается!

Решение через internal: Компилятор Go запрещает импорт пакетов из internal извне модуля.

// Это НЕ скомпилируется, если вызывается из другого модуля: import "github.com/company/project/internal/auth" // ОШИБКА!

Как работает механизм: Go compiler проверяет путь импорта. Если он содержит /internal/, то импорт разрешен только для пакетов, которые находятся в том же поддереве до первого internal.

github.com/company/project/
├── cmd/server/main.go        ✅ Может импортировать internal
├── pkg/api/client.go         ✅ Может импортировать internal
├── internal/auth/auth.go
└── docs/

github.com/other/project/
└── main.go                   ❌ НЕ может импортировать internal

Философия: Четкое разделение между "публичным контрактом" и "деталями реализации".

Когда использовать:

  • Код, который не должен импортироваться извне
  • Бизнес-логика приложения
  • Внутренние утилиты и хелперы
  • Экспериментальные API, которые могут измениться

Когда НЕ стоит использовать:

  • Маленькие проекты (< 500 строк)
  • Код, который может быть полезен другим проектам
  • Стабильные утилиты, которые можно вынести в отдельную библиотеку

/pkg - Публичные библиотеки

Осторожно: Многие разработчики злоупотребляют /pkg. Используйте только для кода, который действительно должен быть публичным API.

project/
├── pkg/
│   ├── logger/           // Переиспользуемый логгер
│   │   ├── logger.go
│   │   └── logger_test.go
│   └── validator/        // Валидация данных
│       ├── validator.go
│       └── rules.go
└── ...

Мотивация и философия /pkg (и почему это спорно)

Из всех соглашений о структуре, директория /pkg является источником наибольшей путаницы и споров. В большинстве случаев она вам не нужна.

Откуда появилась /pkg?

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

Основная идея /pkg: это директория для кода, который могут использовать другие проекты. То есть, это публичная библиотека, которую вы предоставляете внешнему миру.

Давайте рассмотрим на конкретных примерах, когда это хорошо, а когда — плохо.


❌ Когда /pkg — это плохая идея

Многие разработчики ошибочно используют /pkg как общую папку для всего кода, который не является main.go.

Пример НЕПРАВИЛЬНОЙ структуры:

my-web-app/
├── cmd/
│   └── main.go
├── internal/
│   └── database/
│       └── db.go
├── pkg/                  // 👈 НЕПРАВИЛЬНОЕ ИСПОЛЬЗОВАНИЕ
│   ├── handlers/         // Обработчики только для этого приложения
│   │   └── user.go
│   ├── models/           // Модели только для этого приложения
│   │   └── user.go
│   └── utils/            // Утилиты только для этого приложения
│       └── helpers.go
└── go.mod

Почему это плохо?

  1. Логическая ошибка: Код в handlers и models предназначен только для my-web-app. Никакой другой проект не будет его импортировать. Значит, по определению, это не "публичный пакет".
  2. Лишняя вложенность: Пути импорта становятся длиннее без всякой на то причины.
    • import "my-web-app/pkg/handlers"
    • Вместо простого: import "my-web-app/handlers"
  3. Путаница: Новый разработчик, увидев /pkg, может подумать, что этот код используется где-то ещё. Это усложняет рефакторинг, так как создается ложное ощущение "публичного API".

Как правильно для этого случая?

Если код не предназначен для импорта другими проектами, его место — в /internal или в корне проекта (если вы планируете его переиспользовать в рамках одного проекта).

Правильная структура для примера выше:

my-web-app/
├── cmd/
│   └── main.go
├── internal/
│   ├── handlers/         // ✅ Вся логика внутри
│   │   └── user.go
│   ├── models/
│   │   └── user.go
│   └── database/
│       └── db.go
└── go.mod

✅ Когда /pkg может быть оправдана

/pkg имеет смысл только тогда, когда ваш репозиторий целенаправленно предоставляет код как библиотеку для внешних потребителей.

Пример ПРАВИЛЬНОЙ структуры:

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

company-libraries/
├── pkg/
│   ├── logger/           // ✅ Публичный, стабильный логгер
│   │   ├── logger.go     // Используется сервисами A, B, C
│   │   └── logger_test.go
│   ├── metrics/          // ✅ Публичная библиотека метрик
│   │   └── prometheus.go // Используется сервисами A, B, C
│   └── api-client/       // ✅ Клиент для внутреннего API
│       └── client.go
├── internal/
│   └── transport/        // Внутренняя логика для api-client,
│       └── http.go       // которую мы не хотим показывать
└── go.mod

Почему это хорошо?

  1. Четкое намерение: Структура ясно говорит: "Этот репозиторий предоставляет три библиотеки: logger, metrics и api-client".
  2. Стабильность: Код в /pkg воспринимается как публичный API. Это означает, что вы не должны вносить в него изменения, которые сломают обратную совместимость.
  3. Разделение: Внутренняя логика, например, транспортный уровень для api-client, скрыта в /internal и недоступна для импорта извне.

Проверочный список: Нужна ли мне /pkg?

Прежде чем создавать директорию /pkg, задайте себе эти вопросы:

  1. Будет ли код в этой папке импортироваться другими Go модулями (репозиториями)?
    • Если нет -> не используйте /pkg. Поместите код в /internal.
  2. Является ли этот код частью публичного API, который я обязуюсь поддерживать?
    • Если нет -> не используйте /pkg.
  3. Упрощает ли /pkg структуру или просто добавляет еще один уровень вложенности?
    • Если добавляет вложенность без пользы -> не используйте /pkg.

Золотое правило: Начинайте без /pkg. Помещайте весь приватный код вашего приложения в /internal, сгруппировав его по функциональности. Вы всегда сможете добавить /pkg позже, если для этого появятся веские причины.

Другие полезные директории

project/
├── api/                  // OpenAPI/Swagger спецификации
│   └── v1/
│       └── openapi.yaml
├── configs/              // Конфигурационные файлы
│   ├── development.yaml
│   └── production.yaml
├── scripts/              // Скрипты сборки и развертывания
│   ├── build.sh
│   └── deploy.sh
├── docs/                 // Документация
│   └── architecture.md
└── test/                 // Интеграционные тесты
    └── integration/
        └── api_test.go

Принципы организации пакетов

1. Группировка по функциональности, а не по типу

Плохо (группировка по типу):

project/
├── models/       // Все структуры данных
├── handlers/     // Все HTTP обработчики
├── services/     // Вся бизнес-логика
└── repositories/ // Весь код работы с БД

Хорошо (группировка по функциональности):

project/
├── internal/
│   ├── user/         // Все, что связано с пользователями
│   │   ├── model.go
│   │   ├── handler.go
│   │   ├── service.go
│   │   └── repository.go
│   └── order/        // Все, что связано с заказами
│       ├── model.go
│       ├── handler.go
│       ├── service.go
│       └── repository.go

Почему группировка по функциональности лучше?

Проблема группировки по типу:

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

// Добавляем функцию "управление продуктами"
// Нужно изменить файлы в 4 разных местах:

models/product.go         // +Product struct
handlers/product.go       // +ProductHandler
services/product.go       // +ProductService
repositories/product.go   // +ProductRepository

// При любом изменении логики продуктов - снова 4 файла!

Проблемы в команде:

  • При работе над одной функцией разработчики изменяют файлы в разных директориях
  • Сложно найти все части одной функциональности
  • Конфликты при слияниях веток (merge conflicts)
  • Неясно, где искать код для конкретной фичи

Преимущества группировки по функциональности:

// Добавляем функцию "управление продуктами"
// Все в одном месте:

internal/product/
├── model.go       // Product struct
├── handler.go     // HTTP handlers
├── service.go     // Business logic
├── repository.go  // Database operations
└── product_test.go // Все тесты здесь же

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

  • Связность: Связанный код находится рядом
  • Понятность: Сразу видно, какие функции есть в системе
  • Изолированность: Изменения в одной функции не влияют на другие
  • Командная работа: Разные разработчики могут работать с разными функциями без конфликтов

Практический пример разницы:

// Плохо: нужно импортировать из разных мест import ( "project/models" "project/services" "project/repositories" ) func CreateUser(userData UserRequest) { user := models.User{...} // Откуда эта структура? err := repositories.SaveUser(user) // А где этот код? services.SendWelcomeEmail(user) // И этот тоже где-то далеко } // Хорошо: все понятно из импорта import "project/internal/user" func CreateUser(userData UserRequest) { // Все функции user находятся в одном пакете user := user.New(userData) err := user.Save() user.SendWelcomeEmail() }

Философия Go: Пакеты должны представлять функциональные области, а не технические слои.

2. Избегайте циклических зависимостей

Go запрещает циклические импорты. Проектируйте архитектуру как направленный ациклический граф (DAG).

// Правильная иерархия зависимостей
main.go
  ↓
internal/api/
  ↓
internal/service/
  ↓
internal/repository/
  ↓
internal/database/

Почему Go запрещает циклические зависимости?

Техническая причина: Компилятор Go компилирует пакеты в определенном порядке. При циклических зависимостях невозможно определить, какой пакет компилировать первым.

// Это НЕ скомпилируется: // package a import "project/b" func DoA() { b.DoB() } // package b import "project/a" func DoB() { a.DoA() } // Компилятор: "Что компилировать сначала - a или b?"

Философская причина: Циклические зависимости указывают на плохую архитектуру. Если два пакета зависят друг от друга, возможно, они должны быть одним пакетом или нужна реорганизация.

Типичные проблемы и решения:

Проблема 1: Взаимные вызовы

// user/user.go import "project/order" func (u *User) GetOrders() []order.Order { ... } // order/order.go import "project/user" func (o *Order) GetUser() user.User { ... } // ❌ Цикл: user → order → user

Решение 1: Интерфейсы

// order/order.go type UserGetter interface { GetUser(id string) User } type Order struct { userGetter UserGetter // Не импортируем user пакет! } // main.go - связываем зависимости userService := user.NewService() orderService := order.NewService(userService)

Проблема 2: Общие типы

// user/user.go import "project/order" // Нужен Order тип // order/order.go import "project/user" // Нужен User тип // ❌ Цикл из-за общих типов

Решение 2: Общий пакет типов

// types/types.go (или models/models.go) type User struct { ... } type Order struct { ... } // user/user.go import "project/types" func ProcessUser(u types.User) { ... } // order/order.go import "project/types" func ProcessOrder(o types.Order) { ... }

Решение 3: Dependency Inversion

// Вместо прямых зависимостей используем интерфейсы // service/user.go type UserService struct { orderRepo OrderRepository // Интерфейс, не конкретный тип } type OrderRepository interface { GetOrdersByUser(userID string) []Order } // repository/order.go - реализует интерфейс type PostgresOrderRepo struct { ... } func (r *PostgresOrderRepo) GetOrdersByUser(userID string) []Order { ... }

Инструменты для обнаружения циклов:

# Проверка зависимостей go mod graph | grep -E "(internal|pkg)" # Визуализация (если установлен graphviz) go mod graph | dot -T svg -o deps.svg

3. Принцип единственной ответственности для пакетов

Каждый пакет должен иметь одну четкую ответственность:

// internal/email/ - только для работы с email package email type Sender interface { Send(to, subject, body string) error } type SMTPSender struct { host string port int } func (s *SMTPSender) Send(to, subject, body string) error { // Реализация отправки email return nil }

4. Интерфейсы определяйте там, где используете

// internal/notification/service.go package notification // Определяем интерфейс там, где используем type EmailSender interface { Send(to, subject, body string) error } type Service struct { emailSender EmailSender } func NewService(emailSender EmailSender) *Service { return &Service{emailSender: emailSender} }

Практические примеры структур

Веб-приложение

ecommerce-api/
├── cmd/
│   ├── api/
│   │   └── main.go
│   └── worker/
│       └── main.go
├── internal/
│   ├── api/
│   │   ├── middleware/
│   │   ├── routes/
│   │   └── handlers/
│   ├── domain/
│   │   ├── user/
│   │   ├── product/
│   │   └── order/
│   ├── infrastructure/
│   │   ├── database/
│   │   ├── cache/
│   │   └── queue/
│   └── config/
├── api/
│   └── v1/
│       └── openapi.yaml
├── migrations/
├── configs/
├── go.mod
└── go.sum

CLI утилита

git-analyzer/
├── cmd/
│   ├── analyze/
│   │   └── main.go
│   └── report/
│       └── main.go
├── internal/
│   ├── git/
│   │   ├── parser.go
│   │   └── repository.go
│   ├── analyzer/
│   │   ├── complexity.go
│   │   └── metrics.go
│   └── reporter/
│       ├── console.go
│       └── json.go
├── testdata/
├── go.mod
└── go.sum

Библиотека

math-utils/
├── calculator/
│   ├── basic.go
│   ├── advanced.go
│   └── calculator_test.go
├── geometry/
│   ├── shapes.go
│   ├── area.go
│   └── geometry_test.go
├── statistics/
│   ├── mean.go
│   ├── median.go
│   └── statistics_test.go
├── examples/
│   └── main.go
├── go.mod
└── go.sum

Типичные ошибки и как их избежать

1. Преждевременная оптимизация структуры

Ошибка: Создание сложной структуры с самого начала

new-project/
├── cmd/
├── internal/
│   ├── domain/
│   ├── infrastructure/
│   ├── application/
│   └── interfaces/
├── pkg/
└── api/

Решение: Начинайте просто и развивайте по мере роста

2. Злоупотребление пакетом utils

Ошибка: Создание пакета-свалки

// internal/utils/utils.go package utils func FormatDate(date time.Time) string { ... } func HashPassword(password string) string { ... } func ValidateEmail(email string) bool { ... } func CalculateDistance(lat1, lon1, lat2, lon2 float64) float64 { ... }

Решение: Создавайте специализированные пакеты

internal/
├── datetime/
│   └── formatter.go
├── auth/
│   └── password.go
├── validation/
│   └── email.go
└── geo/
    └── distance.go

3. Слишком глубокая вложенность

Ошибка: Чрезмерная иерархия

internal/domain/entities/user/model/struct/definition/user.go

Решение: Максимум 3-4 уровня вложенности

internal/user/model.go

4. Неправильное использование internal

Ошибка: Использование internal для маленьких проектов

simple-cli/
├── internal/     // Не нужно для CLI из 200 строк
│   └── logic/
│       └── process.go
└── main.go

Решение: Для простых проектов достаточно одного пакета

simple-cli/
├── main.go
├── process.go
└── process_test.go

Рекомендации для разных типов проектов

Микросервисы

payment-service/
├── cmd/
│   └── server/
│       └── main.go
├── internal/
│   ├── api/           // HTTP API
│   ├── grpc/          // gRPC API
│   ├── payment/       // Бизнес-логика
│   ├── repository/    // Работа с БД
│   └── config/        // Конфигурация
├── proto/             // gRPC определения
├── migrations/
└── docker/

Монолитные приложения

ecommerce-monolith/
├── cmd/
│   ├── web/           // Веб-приложение
│   ├── api/           // REST API
│   └── admin/         // Административный интерфейс
├── internal/
│   ├── user/          // Управление пользователями
│   ├── product/       // Управление товарами
│   ├── order/         // Управление заказами
│   ├── payment/       // Обработка платежей
│   ├── shared/        // Общие компоненты
│   └── infrastructure/
├── web/
│   ├── static/
│   └── templates/
└── migrations/

CLI утилиты

deployment-tool/
├── cmd/
│   ├── deploy/
│   ├── rollback/
│   └── status/
├── internal/
│   ├── kubernetes/
│   ├── docker/
│   ├── config/
│   └── logger/
├── configs/
│   └── templates/
└── docs/

Инструменты и автоматизация

Генерация структуры проекта

Используйте инструменты для автоматического создания структуры:

# Создание стандартной структуры go mod init github.com/username/project mkdir -p cmd/server internal pkg

Линтеры для проверки структуры

# .golangci.yml linters: enable: - goimports # Проверка импортов - unused # Неиспользуемый код - gosec # Безопасность - ineffassign # Неэффективные присвоения

Автоматическая проверка зависимостей

# Проверка циклических зависимостей go mod graph | grep -E "(internal|pkg)" | sort | uniq

Миграция существующих проектов

Поэтапный подход

  1. Анализ текущей структуры: Определите функциональные области
  2. Создание новой структуры: Начните с основных директорий
  3. Постепенный перенос: Переносите код по одному пакету
  4. Обновление импортов: Используйте инструменты автоматического рефакторинга
  5. Тестирование: Убедитесь, что все работает корректно

Пример миграции

До:

old-project/
├── main.go           // 500 строк
├── handlers.go       // 300 строк
├── database.go       // 200 строк
└── utils.go          // 150 строк

После:

new-project/
├── cmd/
│   └── server/
│       └── main.go
├── internal/
│   ├── api/
│   │   └── handlers.go
│   ├── database/
│   │   └── postgres.go
│   └── utils/
│       └── helpers.go
└── go.mod

Заключение

Хорошая структура проекта экономит время и нервы в долгосрочной перспективе. Но не усложняйте всё с самого начала.

Основные принципы:

  1. Начинайте с main.go - добавляйте структуру по мере роста проекта
  2. Группируйте код по функциональности - user, product, order (а не models, handlers, services)
  3. Используйте /internal осознанно - не для каждого маленького проекта
  4. Избегайте глубокой вложенности - больше 3-4 уровней усложняет навигацию
  5. Проверяйте себя простым вопросом: "Где найти код для функции X?" Если ответ не очевиден - пересмотрите структуру.

Краткая шпаргалка:

Размер проектаЧто использовать
< 200 строкmain.go + README.md
200-1000 строкmain.go + несколько .go файлов
1000+ строкinternal/ пакеты по функциональности
Несколько приложений/cmd + /internal
Библиотека для другихПакеты в корне, без /internal

Помните: если структура мешает работе, а не помогает - значит, пора её упростить.

Полезные ресурсы

© 2025 Praxis. Все права защищены.