
Полная версия
Full stack Developer
Потом начинаешь ценить: «о, тут сложно написать слишком хитро».
Практический эффект:
– проще ревьюить код,
– проще онбордить людей,
– меньше «архитектурных религий» внутри команды.
Go как будто говорит: «давайте решим задачу, а не устроим конкурс абстракций».
Плюс 2. Быстрые бинарники и низкое потребление ресурсов
Go компилируется в один бинарник. Это даёт:
– простой деплой (особенно в контейнерах),
– меньше проблем с окружением,
– быстрый старт сервиса,
– хорошую эффективность по памяти и CPU (обычно).
Для платформенных команд это почти музыка:
– один файл,
– понятная конфигурация,
– легко катать в Kubernetes,
– быстро поднимать новые инстансы.
Если у вас много микросервисов, это ощущается особенно сильно: меньше инфраструктурной «возни».
Плюс 3. Отличная конкурентность: goroutines и каналы
Go создавался с мыслью, что сетевой сервер – это история про параллельность.
goroutines – очень лёгкие «зелёные потоки», которые Go runtime планирует сам.
В итоге вы можете:
– держать много одновременных соединений,
– параллелить I/O операции,
– строить пайплайны обработки.
Это не означает, что багов конкурентности не будет. Будут, конечно. Но модель проще, чем ручная работа с потоками во многих языках.
Часто Go выбирают именно за эту «естественность» конкурентного кода: сделал `go func()` – и задача поехала параллельно.
Плюс 4. Идеален для сетевых сервисов, инфраструктуры, high-load API
Go отлично показывает себя там, где нужно:
– много сетевых запросов,
– простая и быстрая обработка,
– низкие задержки,
– предсказуемость,
– эффективность на сервере.
Поэтому Go часто встречается:
– в API-шлюзах,
– в сервисах авторизации,
– в обработке событий,
– в инфраструктурных компонентах,
– в высоконагруженных микросервисах.
4.4. Минусы Go
Минус 1. Меньше «встроенной выразительности» для доменной логики (чем в Java)
Если доменная область сложная: много правил, много сущностей, много вариаций поведения – Java с её объектной моделью и типами иногда выражает такие вещи удобнее.
Go чаще тянет к стилю:
– простые структуры,
– явные функции,
– минимум «магии».
Это хорошо для инфраструктуры, но в сложной доменной логике вы можете столкнуться с тем, что:
– код начинает расползаться по функциям,
– появляются самодельные паттерны,
– хочется более богатой типовой системы «из коробки».
Это не «Go плох», это просто другой стиль. Но этот стиль подходит не всем доменам.
Минус 2. ORM/генерации/валидации – нужно выбирать аккуратно
В Go есть инструменты для:
– ORM,
– миграций,
– валидации,
– генерации клиентов/серверов.
Но важный нюанс: в Go-экосистеме есть много подходов, и не все одинаково зрелые и совместимые друг с другом. Можно легко собрать «зоопарк».
Что часто происходит:
– один пакет для роутинга,
– другой – для валидации,
– третий – для генерации,
– четвёртый – для БД,
– и всё это по-разному считает ошибки, контексты и структуру проекта.
Это лечится выбором «консервативного» набора инструментов и стандартов. Но новичку можно запутаться.
Минус 3. Дженерики есть, но экосистема ещё догоняет удобства Java/TS
В Go появились дженерики, и это полезно. Но:
– многие библиотеки и команды ещё адаптируются,
– часть паттернов в Go всё равно остаётся «ручной работой»,
– типовая система Go не пытается стать Java или TypeScript – она остаётся проще.
Если вы привыкли к богатому миру типовых абстракций, Go может казаться «немного деревянным». Иногда это плюс. Иногда – раздражает.
4.5. Когда выбирать Go
Сценарий 1. Высокая нагрузка, микросервисы, platform/infrastructure
Go – отличный выбор, если вы строите:
– много небольших сервисов,
– инфраструктурные компоненты,
– прокси, gateway, обработчики событий,
– high-load API с понятной логикой.
Там, где нужно «работает быстро и стабильно», Go часто оказывается в верхней части списка.
Сценарий 2. Когда важны простые деплои и эффективность
Если вам важно:
– быстро собирать и доставлять сервисы,
– легко масштабироваться,
– иметь минимальные зависимости,
– экономить ресурсы,
то Go очень часто выигрывает.
Сценарий 3. Когда команда ценит простоту и единый стиль
Если у вас команда, которая хочет:
– понятный код,
– минимум магии,
– предсказуемую сборку,
Go хорошо поддерживает этот стиль на уровне языка.
4.6. Вывод по Go
Go – это выбор, когда вам нужно:
– быстро,
– просто,
– эффективно,
– легко деплоить,
– хорошо работать под конкуренцией и нагрузкой.
Он может быть менее удобен для «богатой» доменной модели, зато очень хорош как рабочая лошадь для сетевых сервисов и инфраструктуры.
Глава 5. Сводные таблицы сравнения
Ниже – таблицы, которые помогают быстро «примерить» язык под задачу.
Важно: это не абсолютная истина, а практическая оценка типичного стека:
– Python: FastAPI + стандартные практики типизации/линтинга
– Java: Spring Boot
– Go: стандартный `net/http` + распространённые библиотеки
Оценки условные: низко / средне / высоко, иногда с пояснением.
5.1. DX (скорость разработки)
Комментарий:
– Python быстрее всего даёт результат «вчера».
– Java даёт результат «надёжно и по стандарту», но нужно разогнаться.
– Go часто даёт быстрый старт, но прототипирование бизнес-логики может быть медленнее, чем в Python.
5.2. Производительность и latency
Комментарий:
– Python может быть быстрым в I/O, но плохо переносит CPU‑тяжёлое в запросе.
– Java и Go – сильный выбор под нагрузку, но у Java может быть «дороже старт» и больше настройка, а Go обычно проще и легче.
5.3. Типизация и рефакторинг
Комментарий:
– В Python типизация работает, когда команда её реально использует.
– В Java архитектура часто держится на типах автоматически.
– Go строгий, но типовая система проще: меньше «выразительных конструкций», зато меньше сюрпризов.
5.4. Экосистема библиотек
Комментарий:
– Python выигрывает в data/ML и автоматизации.
– Java выигрывает в энтерпрайзе и интеграциях «как в банке».
– Go выигрывает в инфраструктуре и сетевых утилитах.
5.5. Наблюдаемость, диагностика, профилирование
Комментарий:
Java особенно сильна в эксплуатации больших систем: много стандартных инструментов, привычных практик. Go тоже очень неплох благодаря pprof и предсказуемому рантайму. Python требует более аккуратной инженерии (особенно при async).
5.6. Найм и доступность инженеров
Комментарий:
Python знают многие, но «Python для продакшена под нагрузкой» – уже не у всех. Java-инженеров много и часто с опытом больших систем. Go-инженеров меньше, но они часто приходят из high-load или инфраструктуры.
5.7. Риски и типовые «подводные камни»
Итог: как пользоваться этими главами
Если вы выбираете язык под сервис, задайте себе несколько честных вопросов:
1) Сколько лет будет жить система?
2) Насколько сложная доменная логика?
3) Какая нагрузка и какие требования к latency?
4) Насколько важны быстрые итерации и прототипы?
5) Какая у вас команда сейчас и кого реально нанять через 3–6 месяцев?
Дальше выбор обычно становится понятнее.
А если всё равно сложно – это нормально: иногда правильный ответ звучит так:
«Мы берём язык, который команда умеет эксплуатировать без героизма».
Итог: как пользоваться этими главами
Если вы выбираете язык под сервис, задайте себе несколько честных вопросов:
1) Сколько лет будет жить система?
2) Насколько сложная доменная логика?
3) Какая нагрузка и какие требования к latency?
4) Насколько важны быстрые итерации и прототипы?
5) Какая у вас команда сейчас и кого реально нанять через 3–6 месяцев?
Дальше выбор обычно становится понятнее.
А если всё равно сложно – это нормально: иногда правильный ответ звучит так:
«Мы берём язык, который команда умеет эксплуатировать без героизма».
Раздел II. Продукт и требования, единые для всех реализаций.
Ниже три главы одного раздела. Я буду писать так, чтобы это можно было взять как «техническое ТЗ для людей», а не как абстрактную теорию. Мы сначала договоримся что строим, затем – какие системные требования важны, и только потом – зафиксируем API-контракт до кода.
Глава 6. Домен TaskFlow: что строим
TaskFlow – это сервис управления задачами. Если очень коротко: “таски, проекты, комментарии, поиск, роли”. Если чуть длиннее – это маленькая копия того, что люди используют каждый день: Trello/Jira/Asana, но в более компактном виде и с понятным доменом, удобным для обучения.
Мы будем мыслить продуктом: не «сделать таблицы», а «помочь людям работать».
6.1. Главная идея продукта
У пользователя есть рабочие пространства (Workspace). Внутри них – проекты (Project). В проектах – задачи (Task), у задач – комментарии (Comment) и метки (Label).
Плюс обязательные вещи, без которых любой сервис задач быстро превращается в грустный список:
– регистрация и логин,
– права доступа,
– поиск и фильтрация,
– понятные ошибки в API,
– нормальная пагинация,
– аудит действий (кто что сделал).
Опционально (но мы предусмотрим место в дизайне):
– прикрепления файлов,
– уведомления (email/webhook).
> Важно: мы строим учебный, но реалистичный продукт. Поэтому всё, что “потом разберёмся”, мы хотя бы обозначим контрактами и моделями.
6.2. Сущности и их смысл
Ниже – не “таблицы базы данных”, а язык предметной области. Это сильно упрощает жизнь, когда вы будете писать код на любом языке.
6.2.1. User
User – это человек, который входит в систему.
Минимальные поля (логические):
– id – уникальный идентификатор
– email – логин (уникальный)
– password_hash – пароль в виде хеша (никогда не хранить “как есть”)
– name – отображаемое имя
– created_at, updated_at
Что важно сразу:
– email – уникальный.
– пароль не возвращаем через API никогда.
– имя может быть пустым или заполняться позже.
Вопрос продукта: может ли быть один пользователь в нескольких workspace?
Да, иначе сервис слишком ограничен. Это типичная модель “команд”.
6.2.2. Workspace
Workspace – рабочее пространство (команда/организация/группа).
Поля:
– id
– name
– owner_user_id (логический владелец)
– created_at, updated_at
Связи:
– Workspace содержит проекты.
– Workspace содержит участников (membership).
Почему Workspace нужен:
– отделяем проекты разных команд,
– вводим роли и права,
– создаём границы для поиска/доступа.
6.2.3. Роли и права (owner/admin/member)
Роли в рамках workspace:
– owner – главный, может всё, включая удаление workspace и управление правами.
– admin – почти всё, но не «уничтожить мир» (например, не сменить owner).
– member – обычный участник, работает с задачами и проектами по правилам.
Мы должны ответить на вопросы:
– кто может создавать проекты?
– кто может удалять проекты?
– кто может приглашать/удалять участников?
Для учебного проекта фиксируем разумное:
– owner/admin: управление участниками и проектами
– member: CRUD задач и комментариев (в пределах workspace)
– удаление workspace – только owner
> Да, роли всегда вызывают споры. Это нормально. Суть не в идеале, а в ясных правилах.
6.2.4. Project
Project – контейнер задач внутри workspace.
Поля:
– id
– workspace_id
– name
– description (опционально)
– status (например: active/archived)
– created_at, updated_at
Зачем статус:
– “архивировать проект” проще, чем “удалить навсегда”.
– архив не должен мешать поиску по умолчанию (но должен быть доступен через фильтр).
6.2.5. Task
Task – основная единица работы.
Поля (ядро):
– id
– workspace_id (или через project, но удобно иметь прямую привязку)
– project_id
– title
– description (опционально)
– status (todo/in_progress/done/canceled – минимально)
– priority (low/medium/high – опционально)
– assignee_user_id (опционально)
– reporter_user_id (кто создал)
– due_date (опционально)
– created_at, updated_at
Связи:
– Task имеет много комментариев.
– Task имеет много меток (many-to-many).
Сразу оговорим поведение:
– задача всегда принадлежит проекту;
– пользователь должен иметь доступ к workspace проекта, чтобы видеть задачу;
– менять статус может member/admin/owner (если есть доступ).
6.2.6. Comment
Comment – обсуждение задачи.
Поля:
– id
– task_id
– author_user_id
– body
– created_at, updated_at
– опционально: edited_at
Смысл:
– комментарии – это история решений. Поэтому удаление комментариев лучше делать мягким (soft delete) или запрещать полностью. В учебном варианте можно разрешить удаление автору/админу, но в аудит всё равно писать.
6.2.7. Label
Label – метка (тег), используемый для фильтрации.
Поля:
– id
– workspace_id
– name
– color (опционально)
– created_at, updated_at
Почему label живёт в workspace, а не в проекте:
– часто метки общие для всех проектов (“bug”, “feature”, “urgent”).
– проще делать единообразные фильтры.
6.3. Ключевые фичи (и что именно подразумеваем)
6.3.1. Регистрация и логин
Минимальный сценарий:
– пользователь регистрируется по email+паролю
– получает токен (например, JWT или opaque token)
– использует токен для запросов
Важно:
– регистрацию надо защищать от спама и брутфорса (rate limiting – обсудим в главе 7)
– ошибки должны быть понятными, но не слишком разговорчивыми (не рассказывать злоумышленнику, существует ли email)
6.3.2. CRUD проектов и задач
CRUD = Create/Read/Update/Delete, но в реальном продукте:
– “Delete” часто заменяется на архивирование или soft delete
– “Update” – частичное (PATCH), чтобы не отправлять каждый раз всю модель
Для учебной системы мы оставим:
– проекты: создать/получить/обновить/архивировать
– задачи: создать/получить/обновить/удалить (или тоже архивировать – на ваш выбор, но важно быть последовательными)
6.3.3. Поиск, фильтры, сортировка
Это то, что отличает “список задач” от “рабочего инструмента”.
Примеры фильтров для задач:
– по статусу
– по исполнителю
– по метке
– по сроку (due_date)
– по проекту
– по тексту (title/description)
Сортировка:
– по created_at
– по updated_at
– по due_date
– по priority
Поиск:
– простейший: q как подстрока по title/description
– продвинутый: отдельный поисковый движок (не обязательно в первой версии)
Сразу договоримся: фильтры должны быть комбинируемыми, а не “или это, или то”. Это влияет на дизайн API.
6.3.4. Прикрепления файлов (опционально)
Мы можем не реализовать полноценное хранение, но должны понимать модель:
Вариант A (практичный): файлы хранятся в объектном хранилище (S3-подобном), а API хранит только метаданные.
Сущность Attachment (пока концептуально):
– id
– task_id
– uploader_user_id
– filename
– content_type
– size
– storage_key или url
– created_at
Почему это важно даже “опционально”:
– нужно заранее решить: файл лежит у нас или мы выдаём ссылку?
– нужно заранее решить: кто может скачать файл?
– нужно заранее решить: как удалять/истекать ссылки?
6.3.5. Уведомления (email/webhook)
Уведомления почти всегда начинаются с двух событий:
– “задача назначена на меня”
– “в задаче новый комментарий”
Каналы:
– email (человеку)
– webhook (внешней системе: Slack/Teams/что угодно)
Мы будем думать об уведомлениях как о событии:
– событие возникло (task_assigned/comment_added)
– его нужно доставить подписчикам
– доставка может быть не мгновенной
– доставка может повторяться при ошибках
Даже если мы не делаем брокер сообщений в первой версии, мы можем заложить модель и API “подписки на webhooks”.
6.4. Границы доступа и типовые сценарии
Чтобы не сделать “дырявую” систему, полезно проговорить простые правила:
1) Пользователь видит только те workspace, где он участник.
2) Пользователь видит только проекты внутри доступных workspace.
3) Задачи доступны только через доступ к workspace/проекту.
4) Метки – внутри workspace, не глобальные.
5) Комментарии видны только тем, кто видит задачу.
Если вы соблюдаете эти пять пунктов, вероятность “случайно отдать данные другой команды” резко падает.
6.5. Что можно установить для комфортной работы (независимо от языка)
Чтобы не страдать с окружением:
– Docker Desktop или альтернативы (для БД/Redis локально)
– PostgreSQL (обычно удобнее как основная БД)
– DBeaver (просмотр БД)
– HTTP-клиент: Postman / Insomnia / или curl (curl – как хлеб: простой, но питательный)
– Markdown редактор (чтобы вести заметки по контрактам)
6.6. Мини-итог главы
Мы зафиксировали домен и сущности, а главное – общий смысл продукта. Дальше нам нужно договориться, как система должна себя вести под нагрузкой, как мы защищаемся от повторов и ошибок, как ведём аудит.
Глава 7. Нефункциональные требования
Нефункциональные требования – это то, из-за чего продукт либо выглядит профессионально, либо выглядит как “оно работало у меня на ноутбуке”.
В TaskFlow мы добавим набор требований, которые встречаются почти везде:
– rate limiting,
– идемпотентность (как паттерн для POST и “платёжных” операций),
– audit log,
– пагинация (cursor vs offset),
– SLA и наблюдаемость (observability).
Это всё звучит серьёзно, но в реальности это просто набор договорённостей и инструментов.
7.1. Rate limiting (ограничение частоты запросов)
Зачем нужно
Если вы открываете регистрацию и логин в интернет, то к вам придут:
– боты,
– перебор паролей,
– странные сканеры,
– и пользователи, которые “обновляют страницу каждые 200 мс”, потому что им тревожно.
Rate limiting решает две задачи:
1) Защищает инфраструктуру (CPU/DB/сеть).
2) Защищает пользователей (брутфорс на логин/пароль).
Что именно лимитируем
Минимум для TaskFlow:
– /auth/register – лимит на IP и/или на email
– /auth/login – лимит на IP и на аккаунт (email)
– “тяжёлые” списки (поиск задач) – лимит на пользователя
Как выражаем лимит
Удобно мыслить в терминах:
– N запросов за T секунд (например, 10 запросов/мин)
– отдельные лимиты для разных endpoint’ов
Пример политики (условно):
– регистрация: 5/час на IP
– логин: 10/10 минут на IP + 5/10 минут на email
– поиск задач: 60/мин на пользователя
Что возвращаем клиенту
Если лимит превышен, стандартно:
– HTTP 429 Too Many Requests
– заголовок Retry-After (сколько ждать)
И важный момент: сообщение в ответе не должно быть токсичным.
“Too many requests, retry later” – нормально. “Вы слишком много хотите” – это уже лишнее.
7.2. Idempotency для POST (и “платёжных” операций как паттерн)
Идемпотентность – это способность повторить запрос и получить тот же результат, не создав дубликатов.
Почему это важно:
– сеть ненадёжна,
– клиент может не получить ответ и повторить запрос,
– прокси может повторить запрос,
– пользователь может нажать “создать” дважды (классика жанра).
Где нужна идемпотентность
В TaskFlow критично для:
– POST /tasks (создание задач)
– POST /projects
– любые “операции эффекта” (например, отправка webhook/уведомления, загрузка файла – по дизайну)
Идея из платежей:
– клиент отправляет Idempotency-Key
– сервер по этому ключу понимает: это повтор или новый запрос
– повтор возвращает прежний результат
Как это выглядит
Клиент отправляет заголовок:
– Idempotency-Key:
Сервер:
1) проверяет, есть ли запись по ключу + пользователю + endpoint’у
2) если есть – возвращает сохранённый результат (например, 201 и тело созданной задачи)
3) если нет – выполняет операцию, сохраняет результат и возвращает
Важные детали
– Ключ должен быть уникальным на сторону клиента (обычно UUID).
– Хранить идемпотентные ключи надо с TTL (например, 24 часа).
– Нужно привязывать ключ к пользователю (или workspace), иначе один пользователь сможет “повторить” чужую операцию.
Что сохранять
Минимально:
– статус-код,
– response body,
– время создания,
– fingerprint запроса (опционально).
И ещё правило: если по тому же ключу пришёл другой запрос (другая payload), можно вернуть 409 Conflict. Это честно и защищает от странных багов клиента.
7.3. Audit log (журнал аудита)
Аудит – это ответ на вопрос: кто и что сделал.
Даже если вы не банк, аудит полезен:
– для разборов инцидентов,
– для поддержки пользователей,
– для безопасности (“кто удалил проект?”),
– для аналитики.
Что писать в аудит
События минимум:
– user зарегистрировался
– user вошёл (логин)
– создан/обновлён/архивирован проект
– создана/обновлена/удалена задача
– добавлен/изменён/удалён комментарий
– изменения ролей и участников workspace
Структура записи (концептуально)
AuditEvent:
– id
– workspace_id (если событие в workspace)
– actor_user_id (кто сделал)
– action (например: task.created, project.archived)
– entity_type и entity_id
– timestamp
– metadata (JSON: старые/новые значения, ip, user-agent и т.п.)
Где хранить
Для учебного проекта:
– отдельная таблица в основной БД – нормально.
В больших системах:
– иногда события идут в отдельное хранилище/лог-систему.
Тонкий момент: PII
Если у вас есть персональные данные, не пишите в аудит лишнее.
Например, пароль и токены – никогда.
Email – можно, но лучше как идентификатор, не как “лог всего”.
7.4. Pagination: cursor vs offset (сравнение и выбор)
Списки задач, комментариев, проектов – это обязательно пагинация.
Есть два основных подхода:
– Offset pagination: limit=20&offset=40
– Cursor pagination: limit=20&cursor=…
Offset pagination
Плюсы:
– простая для понимания,
– легко прыгать на “страницу 5”.
Минусы:
– на больших данных может быть медленнее (offset заставляет БД “пропускать” строки),
– нестабильность при изменениях: если между запросами добавили записи, страница “поплывёт”.
Cursor pagination
Плюсы:
– стабильнее при добавлениях/удалениях,
– обычно лучше по производительности на больших объёмах,
– идеально подходит для “бесконечной ленты”.
Минусы:
– сложнее для клиентов,
– “страница 5” как концепция исчезает (есть только “вперёд/назад”, если реализовано).
Что выбираем для TaskFlow
Реалистичный выбор:
– для задач и комментариев: cursor pagination
– для простых справочников (labels, members) можно offset, но лучше быть последовательными
Cursor обычно строится на:
– (created_at, id) как “ключ сортировки”
– курсор кодируется в base64 и передаётся как строка
Пример сортировки:
– “новые сверху”: сортируем по created_at DESC, id DESC
– курсор хранит последнюю запись текущей страницы
7.5. SLA и Observability (наблюдаемость)
Что такое SLA в нашем контексте
SLA обычно формализуют в процентах доступности (“99.9%”). Для учебного проекта важнее другая мысль:
> Мы должны уметь доказать, что сервис жив, и быстро понять, если он болен.
Минимальные SLO (цели) для API:
– p95 latency для основных endpoint’ов, например < 300–500 мс (условно)
– error rate < 1%
– доступность (uptime) по health-check
Что нужно из observability
Минимальный набор, который стоит закладывать:
– структурированные логи (JSON, с request_id)









