Проектирование структуры
Сложный проект меняет не код — он меняет порядок, в котором ты думаешь о коде.
Повезло оказаться в большом проекте Мастерской, в процессе которого я начал иначе смотреть на то, с чего начинается реализация. Раньше казалось: разберусь с архитектурой, напишу репозитории, потом сервисы, потом хендлеры. Логично же? Нет.
Писать код — это про понимание структуры, инфраструктуры и надёжности системы до первой бизнес-строчки.
Конкретно это выглядит так:
1. Скелет — приложение запускается и корректно гасится. Без логики.
2. Конфиги — структуры по сущностям, дефолты, валидация на старте.
3. Зависимости — логгер, кэш, метрики. До бизнес-логики, не во время.
Почему не во время? Потому что вот так начинают:
Правильно — решить это один раз и не возвращаться:
4. Первый сквозной путь — один рабочий сценарий end-to-end. Всё остальное заглушки.
5. Скелет тестов — точка входа для интеграционных. Не сами тесты — структура.
6. Рост — новый функционал по уже выстроенному шаблону.
Сложный проект меняет не код — он меняет порядок, в котором ты думаешь о коде.
Повезло оказаться в большом проекте Мастерской, в процессе которого я начал иначе смотреть на то, с чего начинается реализация. Раньше казалось: разберусь с архитектурой, напишу репозитории, потом сервисы, потом хендлеры. Логично же? Нет.
Писать код — это про понимание структуры, инфраструктуры и надёжности системы до первой бизнес-строчки.
Конкретно это выглядит так:
1. Скелет — приложение запускается и корректно гасится. Без логики.
2. Конфиги — структуры по сущностям, дефолты, валидация на старте.
3. Зависимости — логгер, кэш, метрики. До бизнес-логики, не во время.
Почему не во время? Потому что вот так начинают:
func New(db *pgxpool.Pool) *App { ... }
// потом логгер
func New(db *pgxpool.Pool, logger *zap.Logger) *App { ... }
// потом кэш, метрики, конфиг...
func New(db *pgxpool.Pool, logger *zap.Logger, cache *redis.Client, cfg *Config) *App { ... }
// хочется рефакторить?Правильно — решить это один раз и не возвращаться:
type App struct {
db *pgxpool.Pool
cache *redis.Client
logger *zap.Logger
}
func New(cfg *Config) (*App, error) { ... }4. Первый сквозной путь — один рабочий сценарий end-to-end. Всё остальное заглушки.
5. Скелет тестов — точка входа для интеграционных. Не сами тесты — структура.
6. Рост — новый функционал по уже выстроенному шаблону.
❌ Горизонтально:
[все репо] → [все сервисы] → [все хендлеры]
↳ долго ждёшь первого рабочего результата
✅ Вертикально:
[скелет] → [конфиги] → [зависимости] → [1 сценарий] → рост
↳ каждый шаг даёт обратную связь
❤1
Окно возможностей
Постоянное саморазвитие — это хорошо. Но без переключения энергия иссякнет. Что вас заряжает?
В ожидании следующего проекта прошёлся seq scan по составленной roadmap. Через index scan — погрузился в проблемные темы. Несколько недель часы напролёт — и в какой-то момент мозг просто перестаёт брать новое: нужна понятная задача, не просто поток информации.
Ресурс есть у всех. Мой стиль — интенсивный: усваиваю больше информации, увеличиваю её поток в единицу времени. Работает — пока не упираешься в стену.
В проекте всё иначе. Акцент на задачах. В сложные моменты — когда держишь большой контекст и принимаешь важные решения — новая информация только добавит шум. Канал и статьи работают наоборот: пишешь пост, структурируешь мысли, голова разгружается.
Без проекта контекст уменьшается — и его нужно заполнять. Самое время для нового: теория, практика, алгоритмы. И более глубокие посты в канале — есть время подумать, а не только зафиксировать находку.
prod logs — инструмент в обоих режимах. В проекте разгружает, между проектами — углубляет. И в обоих случаях — карта моего пути в IT.
Постоянное саморазвитие — это хорошо. Но без переключения энергия иссякнет. Что вас заряжает?
В ожидании следующего проекта прошёлся seq scan по составленной roadmap. Через index scan — погрузился в проблемные темы. Несколько недель часы напролёт — и в какой-то момент мозг просто перестаёт брать новое: нужна понятная задача, не просто поток информации.
Ресурс есть у всех. Мой стиль — интенсивный: усваиваю больше информации, увеличиваю её поток в единицу времени. Работает — пока не упираешься в стену.
В проекте всё иначе. Акцент на задачах. В сложные моменты — когда держишь большой контекст и принимаешь важные решения — новая информация только добавит шум. Канал и статьи работают наоборот: пишешь пост, структурируешь мысли, голова разгружается.
Без проекта контекст уменьшается — и его нужно заполнять. Самое время для нового: теория, практика, алгоритмы. И более глубокие посты в канале — есть время подумать, а не только зафиксировать находку.
prod logs — инструмент в обоих режимах. В проекте разгружает, между проектами — углубляет. И в обоих случаях — карта моего пути в IT.
👍2❤1
Паттерн singleflight и delete(g.m, key)
При изучении исходников бросился в глаза один участок кода в
Сразу появился вопрос: мы удаляем ключ — что происходит с данными?
Вспоминаем как работает
А ссылки уже взяты раньше — в
Каждая ожидающая горутина держит локальную копию указателя
func (g *Group) Forget(key string)
Тут и появляется
Новый *call, новый wg — независимо от старого. Старые ожидающие горутины ждут свой *call, новые — свой. Когда старый завершится, doCall проверит g.m[key] == c — увидит там уже другой указатель и не тронет новый вызов.
Проверка на идентичность указателя в doCall — не случайная деталь. Защита от ситуации когда старый вызов завершается позже нового. Осознанный трейдофф: дублирование в обмен на latency.
Исходники: https://github.com/golang/sync/blob/master/singleflight/singleflight.go
#go #sync #gogc
При изучении исходников бросился в глаза один участок кода в
doCall:func (g *Group) doCall(c *call, key string, fn func() (any, error)) {
...
if g.m[key] == c {
delete(g.m, key)
}
...
}Сразу появился вопрос: мы удаляем ключ — что происходит с данными?
Вспоминаем как работает
GC в Go. Он оперирует ссылками: пока на область памяти есть живые ссылки — объект не собирается. delete убирает ключ из мапы, но не объект из памяти.А ссылки уже взяты раньше — в
Do:func (g *Group) Do(key string, fn func() (any, error)) (v any, err error, shared bool) {
...
if c, ok := g.m[key]; ok {
c.dups++
g.mu.Unlock()
c.wg.Wait()
return c.val, c.err, true
}
...
}Каждая ожидающая горутина держит локальную копию указателя
c. Первый вызов завершается, пишет результат в структуру, doCall удаляет ключ из мапы. Все ожидающие спокойно считывают данные через свои указатели, завершаются — и только тогда GC может собрать объект.delete(g.m, key) ≠ удаление данных.func (g *Group) Forget(key string)
Тут и появляется
Forget. Если doCall всё равно почистит мапу сам — зачем он?Forget нужен когда не хочешь ждать завершения текущего вызова. Например, fn зависла — внешний сервис деградировал. Вызываешь Forget — ключ удаляется из мапы прямо сейчас. Следующий Do не встанет в очередь к зависшему вызову, а выполнит в Do до ветки с ожидающими горутинами:c := new(call)
c.wg.Add(1)
g.m[key] = c
Новый *call, новый wg — независимо от старого. Старые ожидающие горутины ждут свой *call, новые — свой. Когда старый завершится, doCall проверит g.m[key] == c — увидит там уже другой указатель и не тронет новый вызов.
Проверка на идентичность указателя в doCall — не случайная деталь. Защита от ситуации когда старый вызов завершается позже нового. Осознанный трейдофф: дублирование в обмен на latency.
Исходники: https://github.com/golang/sync/blob/master/singleflight/singleflight.go
#go #sync #gogc
GitHub
sync/singleflight/singleflight.go at master · golang/sync
[mirror] concurrency primitives. Contribute to golang/sync development by creating an account on GitHub.
«Go: Simple to learn, but hard to master» — так называется первая глава книги 100 Go Mistakes (Teiva Harsanyi).
На примере предыдущего поста про singleflight это хорошо видно. Один паттерн — а за ним: Mutex, WaitGroup, map, указатели, GC. И так в каждом паттерне, в каждом пакете, пока углубляясь не начнёшь ходить по кругу. Ведь всё между собой связано.
Курсы проводят по верхнему слою — дают «знаю». Куда идти дальше и насколько глубоко — зависит только от тебя.
«Знаю» — прочитал про singleflight: дедупликация вызовов по ключу, ждём первого, отдаём всем.
«Понимаю» — могу объяснить что происходит с памятью когда вызывают Forget. Почему GC не тронет объект. Почему в doCall сравнивают указатели, а не ключи.
Второе приходит только через практику + возврат к теории с конкретным вопросом.
На своём опыте с алгоритмами — сначала решал как придётся. Потом пришёл к теории. Акцент сменился — не «решить задачу», а «найти правильный подход».
На примере предыдущего поста про singleflight это хорошо видно. Один паттерн — а за ним: Mutex, WaitGroup, map, указатели, GC. И так в каждом паттерне, в каждом пакете, пока углубляясь не начнёшь ходить по кругу. Ведь всё между собой связано.
Курсы проводят по верхнему слою — дают «знаю». Куда идти дальше и насколько глубоко — зависит только от тебя.
«Знаю» — прочитал про singleflight: дедупликация вызовов по ключу, ждём первого, отдаём всем.
«Понимаю» — могу объяснить что происходит с памятью когда вызывают Forget. Почему GC не тронет объект. Почему в doCall сравнивают указатели, а не ключи.
Второе приходит только через практику + возврат к теории с конкретным вопросом.
На своём опыте с алгоритмами — сначала решал как придётся. Потом пришёл к теории. Акцент сменился — не «решить задачу», а «найти правильный подход».
❤1
9 июня участвую в открытом mock-интервью от Яндекс Практикума — вебинар про алгоритмические собеседования для джунов.
Тимлид команды инфраструктуры RMS в Яндекс Финтехе, штатный интервьюер по алгоритмам — проверит меня и коллегу из Python-направления в прямом эфире. Хорошая возможность для всех получить советы напрямую от человека, который оценивает такие решения каждый день.
Можно посмотреть на практике как выглядит то самое знакомство с алгоритмами, о котором писал выше.
Бесплатно, 19:00 мск.
Регистрация: https://practicum.yandex.ru/promowebinar/8bdd09d4-596e-4ef9-a16f-1849a9c122e9/registration/
Тимлид команды инфраструктуры RMS в Яндекс Финтехе, штатный интервьюер по алгоритмам — проверит меня и коллегу из Python-направления в прямом эфире. Хорошая возможность для всех получить советы напрямую от человека, который оценивает такие решения каждый день.
Можно посмотреть на практике как выглядит то самое знакомство с алгоритмами, о котором писал выше.
Бесплатно, 19:00 мск.
Регистрация: https://practicum.yandex.ru/promowebinar/8bdd09d4-596e-4ef9-a16f-1849a9c122e9/registration/
❤1
Docker для Go сервиса: три уровня
Уровень 1 — просто работает
Образ ~300MB. Внутри весь Go toolchain, исходники, всё что не нужно в проде.
Уровень 2 — multistage + layer cache
Образ уже меньше — toolchain и исходники в финальный слой не попадают.
CA сертификаты нужны если сервис делает HTTPS запросы. Non-root пользователь — минимальная безопасность, которую стоит закладывать сразу.
Уровень 3 — прод
Два добавления.
Mount cache — живёт между сборками независимо от layer cache. На CI где каждый раз чистая машина, модули берутся из volume, а не скачиваются заново. Трейдофф: 200–500MB на диске, не чистится автоматически.
VERSION и BUILD_TIME — sha коммита и время сборки вшиваются в бинарь через ldflags. В GitHub Actions передаётся так:
При старте сервера логируешь — всегда знаешь что задеплоено.
distroless вместо alpine — нет shell, нет пакетного менеджера,
И
А какой 4 уровень? 😉
Уровень 1 — просто работает
FROM golang:1.25-alpine
WORKDIR /app
COPY . .
RUN go build -o server ./cmd/server
CMD ["./server"]
Образ ~300MB. Внутри весь Go toolchain, исходники, всё что не нужно в проде.
Уровень 2 — multistage + layer cache
FROM golang:1.25-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o /app/bin/server ./cmd/server
FROM alpine
RUN apk --no-cache add ca-certificates curl
RUN addgroup -g 1001 -S appgroup && \
adduser -u 1001 -S appuser -G appgroup
COPY --from=builder /app/bin/server .
USER appuser:appgroup
ENTRYPOINT ["./server"]
Образ уже меньше — toolchain и исходники в финальный слой не попадают.
go.mod и go.sum копируются отдельно: если зависимости не менялись — go mod download берётся из layer cache. Работает локально и на self-hosted runner. На GitHub Actions с ephemeral runner — нет, там каждый job на чистой машине. Для этого в уровне 3 появляется mount cache.CA сертификаты нужны если сервис делает HTTPS запросы. Non-root пользователь — минимальная безопасность, которую стоит закладывать сразу.
Уровень 3 — прод
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 go build \
-ldflags "-s -w -X main.version=${VERSION} -X main.buildTime=${BUILD_TIME}" \
-trimpath -o /app/bin/server ./cmd/server
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/bin/server .
USER nonroot:nonroot
ENTRYPOINT ["./server"]
Два добавления.
Mount cache — живёт между сборками независимо от layer cache. На CI где каждый раз чистая машина, модули берутся из volume, а не скачиваются заново. Трейдофф: 200–500MB на диске, не чистится автоматически.
VERSION и BUILD_TIME — sha коммита и время сборки вшиваются в бинарь через ldflags. В GitHub Actions передаётся так:
build-args: |
VERSION=${{ github.sha }}
BUILD_TIME=${{ github.event.head_commit.timestamp }}
При старте сервера логируешь — всегда знаешь что задеплоено.
distroless вместо alpine — нет shell, нет пакетного менеджера,
nonroot встроен. CA сертификаты тоже уже внутри.И
.dockerignore — без него COPY . . тянет .git, тесты, IDE файлы.А какой 4 уровень? 😉
❤2
Насмотренность — это первый уровень
Видишь ошибку — обрабатываешь. Это базовый рефлекс. Но есть второй уровень: понимать, с чем именно работаешь.
Два примера из недавнего PR.
NATS MessageBatch
Итерация прошла, всё обработали. Но
Два момента pgxpool.NewWithConfig
Первый — проглоченная ошибка, классика. Ошибку можно не обрабатывать, если ты понимаешь в каких случаях она возникает. И в go, лучше её обработать несмотря ни на что.
Второй интереснее: зачем функции контекст, если
Видишь ошибку — обрабатываешь. Это базовый рефлекс. Но есть второй уровень: понимать, с чем именно работаешь.
Два примера из недавнего PR.
NATS MessageBatch
for msg := range msgs.Messages() { ... }
// msgs.Error() — не вызвалиИтерация прошла, всё обработали. Но
MessageBatch — не просто итератор. У него есть Error(), который нужно проверить после цикла: батч мог завершиться с ошибкой, и ты об этом не узнаешь. Не думай, что у типа одно поле и один метод — смотри что там есть.Два момента pgxpool.NewWithConfig
pool, _ := pgxpool.NewWithConfig(context.Background(), config)
Первый — проглоченная ошибка, классика. Ошибку можно не обрабатывать, если ты понимаешь в каких случаях она возникает. И в go, лучше её обработать несмотря ни на что.
Второй интереснее: зачем функции контекст, если
Background() и так работает? Потому что при инициализации пул реально устанавливает соединения (MinConns). Контекст позволяет это отменить. Раз параметр есть — значит на него что-то завязано. Не думай, что всё хорошо по умолчанию.❤1👍1
Тимлид: роль или название должности
Крайние посты всё чаще о подходах к разработке. Причина — в команде всё чаще спорим: как решать, почему именно так. По обычаю, вопросы такие поднимаю я.
Не могу назвать себя лидом без оговорки — есть старший коллега, к кому идёшь в спорных ситуациях. Честнее сказать: я сопровождаю команду бэкенда.
Но взгляд на проект у меня уже другой.
Вывод №1. В первом проекте подход был — делать максимально сложно. Redis? Дайте два. Сложный запрос? Почему бы и нет. Чтобы потом ходить рассказывать: я работал с тем и этим.
Сейчас — чем проще, тем лучше. Понимаешь бизнес-задачу, ориентируешься на ожидания заказчика — и не закладываешь функционал на будущее.
Вывод №2. Типизируй. В Go есть такая возможность — пользуйся. Когда данные ходят между функциями, структура вместо россыпи параметров ловит ошибки ещё до компиляции. В команде джунов это особенно заметно.
К чему это всё. Чтобы быть лидом, а не называться им — нужна насмотренность, которая за год не набирается физически. А с управленческой стороны — понимать уровень каждого, оценивать темп, держать в голове, что гарантий результата нет.
Тимлид меньше пишет код и больше смотрит на проект целиком. Когнитивную нагрузку от этого не передать.
#тимлид #go #бэкенд #разработка #команда
Крайние посты всё чаще о подходах к разработке. Причина — в команде всё чаще спорим: как решать, почему именно так. По обычаю, вопросы такие поднимаю я.
Не могу назвать себя лидом без оговорки — есть старший коллега, к кому идёшь в спорных ситуациях. Честнее сказать: я сопровождаю команду бэкенда.
Но взгляд на проект у меня уже другой.
Вывод №1. В первом проекте подход был — делать максимально сложно. Redis? Дайте два. Сложный запрос? Почему бы и нет. Чтобы потом ходить рассказывать: я работал с тем и этим.
Сейчас — чем проще, тем лучше. Понимаешь бизнес-задачу, ориентируешься на ожидания заказчика — и не закладываешь функционал на будущее.
«Совершенство достигнуто не тогда, когда нечего добавить, а тогда, когда нечего убрать»
— Антуан де Сент-Экзюпери, «Планета людей»
Вывод №2. Типизируй. В Go есть такая возможность — пользуйся. Когда данные ходят между функциями, структура вместо россыпи параметров ловит ошибки ещё до компиляции. В команде джунов это особенно заметно.
К чему это всё. Чтобы быть лидом, а не называться им — нужна насмотренность, которая за год не набирается физически. А с управленческой стороны — понимать уровень каждого, оценивать темп, держать в голове, что гарантий результата нет.
Тимлид меньше пишет код и больше смотрит на проект целиком. Когнитивную нагрузку от этого не передать.
#тимлид #go #бэкенд #разработка #команда
❤1
Написал статью на Хабр — про публичный мок-собес по АА-секции от Яндекс Практикума.
Формально это рекламная активность курсов. По факту — опыт, который никакой подготовкой не заменить.
3 недели, ~40 решённых задач с нуля и много пройденных заново, наконец ушёл от решений «в лоб» к пониманию паттернов. А на созвоне дали array длины 6 и k=40 — и код, который прошёл первый кейс, на втором ушёл в панику. Раннего выхода на k > len(nums) просто не было.
Оказалось, что АА-секция проверяет не только знание алгоритмов — она проверяет, видишь ли ты собственные слепые пятна, когда решение уже «работает».
Статья: https://habr.com/ru/articles/1059542/
Формально это рекламная активность курсов. По факту — опыт, который никакой подготовкой не заменить.
3 недели, ~40 решённых задач с нуля и много пройденных заново, наконец ушёл от решений «в лоб» к пониманию паттернов. А на созвоне дали array длины 6 и k=40 — и код, который прошёл первый кейс, на втором ушёл в панику. Раннего выхода на k > len(nums) просто не было.
Оказалось, что АА-секция проверяет не только знание алгоритмов — она проверяет, видишь ли ты собственные слепые пятна, когда решение уже «работает».
Статья: https://habr.com/ru/articles/1059542/
Хабр
Публичный мок АА в Яндексе: опыт, который не заменит никакая подготовка
Предыстория Современный разработчик не упускает возможностей — отказаться от мок-собеса в Яндекс я не смог. Отдельная благодарность двум людям, без которых этого опыта не было бы: Оле Рыбаковой,...
👍3❤1
Прямой переворот интуиции
Считается, что ревью защищает от твоих багов, потому что кто-то другой их находит. На самом деле чаще работает иначе: ты сам находишь свои баги — ревьюя чужой код.
За последнюю неделю поймал так два бага у себя. Не в момент ревью своего кода — разбирая чужой PR.
Механизм простой: ревью — единственный момент, когда ты смотришь на код не как автор, а как критик. И по инерции применяешь этот режим к своему.
Первый баг: неделей раньше забыл прописать префикс
Второй, там же — увидел в чужом PR, как собирается URL внутри модуля, и вспомнил, что у меня не так:
Функции нужен был только токен — сборка url была не её зоной ответственности:
Вывод: ревью выгодно не столько автору PR, сколько тому, кто ревьюит. Отказываясь от ревью чужого кода, ты не экономишь время — ты теряешь единственный момент, где сам смотришь на свой подход со стороны.
Это цена мелкой ошибки — на уровне пары строк. Что бывает, когда без ревью в main проходят решения покрупнее — отдельная история.
#go #codereview #backend #разработка #процессы #инженерка
Считается, что ревью защищает от твоих багов, потому что кто-то другой их находит. На самом деле чаще работает иначе: ты сам находишь свои баги — ревьюя чужой код.
За последнюю неделю поймал так два бага у себя. Не в момент ревью своего кода — разбирая чужой PR.
Механизм простой: ревью — единственный момент, когда ты смотришь на код не как автор, а как критик. И по инерции применяешь этот режим к своему.
Первый баг: неделей раньше забыл прописать префикс
/api/v1 в финальных роутах. Планировал вешать его через группу роутера, но из-за разных обёрток (rate limiter в одних местах, auth в других) решил подождать, пока все ручки устаканятся — и забыл вписать префикс в итоге. Код уже в main, готовимся к деплою.Второй, там же — увидел в чужом PR, как собирается URL внутри модуля, и вспомнил, что у меня не так:
verifyURL := au.cfg.FrontendURL +
"/verify-email?token=" +
base64.RawURLEncoding.EncodeToString(tokenRaw)
Функции нужен был только токен — сборка url была не её зоной ответственности:
verifyURL := base64.RawURLEncoding.EncodeToString(tokenRaw)
Вывод: ревью выгодно не столько автору PR, сколько тому, кто ревьюит. Отказываясь от ревью чужого кода, ты не экономишь время — ты теряешь единственный момент, где сам смотришь на свой подход со стороны.
Это цена мелкой ошибки — на уровне пары строк. Что бывает, когда без ревью в main проходят решения покрупнее — отдельная история.
#go #codereview #backend #разработка #процессы #инженерка
👍2
Рефакторинг на ровном месте
У нас в main оказалось два хелпера для работы с контекстом — и часть ручек из-за этого не работает.
Логгеру нужен был request_id из middleware — прокидывать его в slog.With без контекста не получится, встал вопрос хелпера.
Параллельно я делал auth middleware — тоже клал в контекст, только role и user_id. Задача с логгером подвисла, моя — нет. Я залил свой пакет первым, не дожидаясь хелпера коллеги, которого ещё не было в main.
Позже коллега смёржил логгер — с собственной реализацией контекста, той же проблемой в зеркале: моего пакета к тому моменту уже не заметил.
Не разгильдяйство — просто у параллельной работы своя цена, и в этот раз её заплатили две одинаковые реализации одного хелпера.
Юнит-тесты — зелёные, каждый написан под свой пакет. Но использование контекста в них разное: там, где код ожидает один формат, получает другой — часть данных не прочитать. E2E-тестов, которые прогнали бы запрос через оба варианта, нет.
Будь ревьюеров больше — шанс поймать это раньше был бы выше, до того как поверх обоих пакетов выросли новые задачи.
Ревью — не про стиль кода. Это единственный момент, когда кто-то держит в голове весь проект целиком, а не только свою задачу. Когда таких людей в контексте один-два — вероятность, что нужный тебе функционал уже реализован, а ты об этом не узнаешь, растёт кратно.
#go #codereview #backend #архитектура
У нас в main оказалось два хелпера для работы с контекстом — и часть ручек из-за этого не работает.
Логгеру нужен был request_id из middleware — прокидывать его в slog.With без контекста не получится, встал вопрос хелпера.
Параллельно я делал auth middleware — тоже клал в контекст, только role и user_id. Задача с логгером подвисла, моя — нет. Я залил свой пакет первым, не дожидаясь хелпера коллеги, которого ещё не было в main.
Позже коллега смёржил логгер — с собственной реализацией контекста, той же проблемой в зеркале: моего пакета к тому моменту уже не заметил.
Не разгильдяйство — просто у параллельной работы своя цена, и в этот раз её заплатили две одинаковые реализации одного хелпера.
Юнит-тесты — зелёные, каждый написан под свой пакет. Но использование контекста в них разное: там, где код ожидает один формат, получает другой — часть данных не прочитать. E2E-тестов, которые прогнали бы запрос через оба варианта, нет.
Будь ревьюеров больше — шанс поймать это раньше был бы выше, до того как поверх обоих пакетов выросли новые задачи.
Ревью — не про стиль кода. Это единственный момент, когда кто-то держит в голове весь проект целиком, а не только свою задачу. Когда таких людей в контексте один-два — вероятность, что нужный тебе функционал уже реализован, а ты об этом не узнаешь, растёт кратно.
#go #codereview #backend #архитектура
💯1