Как написать плохой код на Go: антипаттерны и примеры

Антипаттерны Go: реальные примеры плохого кода из production. Блокирующие каналы в HTTP-обработчиках, race conditions, дедлоки, нарушение инкапсуляции. Как не писать код на Go.
- теги
- #Go #Программирование #Архитектура
- категории
- Programming
- опубликовано
Современные разработчики часто пишут на Go, потому что это “быстро” и “круто”. Но знаете что? Можно написать на Go так, что даже легаси PHP-код из нулевых будет выглядеть как произведение инженерного искусства.
Представьте: у вас почтовая система. Массовая рассылка на 100k пользователей. Backend начинает тормозить, HTTP-запросы зависают на минуту, очереди забиваются до потолка, начинаются дедлоки. Вы думаете “MySQL не справляется” или “RabbitMQ медленный” или “нужно больше серверов”.
А на самом деле? Код просто написан через жопу.
Дальше я разберу реальные примеры из production-системы. Это не учебник по хорошему коду — это антипаттерны, которые реально работают в проде. И да, их писали взрослые люди за деньги.
Логирование метрик: блокируем HTTP нахуй
Есть у нас HTTP-эндпоинт, который должен логировать метрики. Надо писать их в канал, а потом батчами в БД. Звучит разумно.
Но вот вопрос: а что если канал переполнен? Может, дропнуть метрику и идти дальше? Может, увеличить буфер? Может, вообще сделать select с default?
Да нахуй. Пусть HTTP-запрос ждет, пока мы запишем в канал. Метрики же важнее пользователя, правда?
type AssetLoadedService struct {
trackChan chan trackData // Буфер 500, этого хватит всем
}
func (this *AssetLoadedService) LogAsset(assetsInfo []requests.AssetInfo) {
for _, item := range assetsInfo {
// Блокирующая запись - пользователь может подождать
this.trackChan <- trackData{
url: item.Url,
contentType: item.ContentType,
size: item.Size,
mailbox: item.Mailbox,
}
}
}
// HTTP-обработчик
func (this *MetricController) AssetLoaded(ctx *gin.Context) {
var body requests.AssetLoadedRequestBody
if err := ctx.ShouldBind(&body); err != nil {
this.error(ctx, common.NewHttpErrorValidationBadRequest(...))
return
}
// Запрос блокируется? Да похуй, пользователи любят подождать
this.assetLoadedService.LogAsset(body.AssetsInfo)
this.response(ctx, []string{})
}
Что происходит при высокой нагрузке? Буфер на 500 элементов улетает за секунды, HTTP-запросы начинают висеть. Пользователь открывает письмо, а в ответ — таймаут. Мониторинг орет. В логах “context deadline exceeded”.
Но зато метрики все записаны! Красота.
Background jobs: race condition как фича
Надо сделать фоновые задачи. Как? Redis Sorted Set? Попробуем setTimeout на фронте сэмулировать? RabbitMQ? Kafka?
Да похуй. Сделаем таблицу background_jobs в древнем как говно мамонта MySQL 5.7 и будем ее селектить без транзакций.
func (this *BackgroundJobManager) fetchingRoutine(pool int) {
jobChannel := make(chan models.BackgroundJobModel) // Небуферизованный, потому что можем
for i := 0; i < this.config.PoolWorkerCount; i++ {
go this.workerRoutine(i, jobChannel)
}
for {
if backgroundJob, success := this.fetchJob(pool); success {
jobChannel <- backgroundJob // Блокируем тут тоже, почему бы нет
time.Sleep(jobFetchAfterJobDuration)
} else {
time.Sleep(jobFetchRetryDuration)
}
}
}
func (this *BackgroundJobRepository) Fetch(pool int) (backgroundJob models.BackgroundJobModel, err error) {
// SELECT без FOR UPDATE - кто нам запретит?
result := this.db.Session(&gorm.Session{Logger: logger.Default.LogMode(logger.Silent)}).
Where("pool = ? AND status = ? AND planned_at <= NOW()", pool, models.BackgroundJobStatusNew).
Order("planned_at ASC, created_at ASC, id ASC").
Limit(1).
Find(&backgroundJob)
// Потом UPDATE отдельным запросом - race condition это просто теория
backgroundJob.Status = models.BackgroundJobStatusInProgress
backgroundJob.StartedAt = &twutils.DateTime{Time: time.Now()}
_ = this.backgroundJobRepository.Update(backgroundJob) // Ошибки? Не слышали
return
}
Что может пойти не так? Да ничего! Разве что:
| Время | Горутина 1 | Горутина 2 |
|---|---|---|
| T1 | SELECT job_id=42 WHERE status=new | |
| T2 | SELECT job_id=42 WHERE status=new | |
| T3 | UPDATE job_id=42 SET status=in_progress | |
| T4 | UPDATE job_id=42 SET status=in_progress | |
| T5 | Обрабатывает джобу | Обрабатывает ту же джобу 💀 |
Обе горутины взяли одну джобу и обрабатывают ее дважды. Но это же редко происходит, правда? Может, будем использовать транзакции и SELECT FOR UPDATE? Может, не будем хуячить с двух коннектов SELECT/UPDATE запросы и не превратим все приложение в однопоток используя redis-lock?
Нахуй нам тогда голанг. Давай возьмем PHP.
Кстати, из веселого: когда начались проблемы, разработчик предложил поставить рейтлимит на 5 сообщений в минуту. Типа не код плохой, а нагрузка большая. ГЕНИЙ.
Архитектура: инкапсуляция которую мы заслужили
На собеседовании нам рассказывали про инкапсуляцию и разделение ответственности. Мы все записали. А теперь делаем backend, который напрямую лезет в БД микросервисов. При этом микросервисы шлют HTTP-запросы на backend.
Как два человека, которые одновременно лезут друг другу в карманы. Инкапсуляция!
// Backend напрямую в БД спулера
type DbConnectionManager struct {
connectionPull map[common.SpoolerIdentityName]*gorm.DB
}
func (this *DbConnectionManager) Get(spooler common.SpoolerIdentityName) (*gorm.DB, error) {
dbDsn := strings.ReplaceAll(this.config.DsnTemplate, "<spooler>", ip.String())
db, err = gorm.Open(mysql.Open(dbDsn), &gorm.Config{...})
return db, nil
}
// Backend читает данные напрямую
func (this *MessageRepository) GetList(spooler common.SpoolerIdentityName, filter filters.MessageFilter) {
db, err := this.GetDB(spooler) // Прямой доступ - быстрее
err = db.Clauses(allClauses...).Find(&messages).Error
}
Может, сделать HTTP API? Может, не давать backend доступ к БД спулеров? Может, разделить ответственность?
Да нахуй. Это же медленнее на 5 мс. А мы делаем хайлоад.
Теперь у нас:
- Backend знает структуру БД спулера (меняешь колонку — меняй backend тоже)
- 100 спулеров = 100 пулов соединений к БД
- Backend → MySQL спулера, спулер → HTTP backend (циклическая зависимость)
- Один SQL injection в backend — утекли все юзеры
Зато быстро! Инкапсуляция — хз что это.
Отложенные задачи: селектим MySQL каждые 50мс
Нам надо сделать отложенное на 10 секунд выполнение задач с возможностью отмены. Как мы это будем делать?
Redis Sorted Set? Попробуем через setTimeout на фронте сэмулировать? Ну что-нибудь намутим в памяти на каналах?
Да похуй. Сделаем таблицу delayed_messages в MySQL 5.7 и будем ее селектить раз в 50мс. Проверяем planned_at <= NOW(), хватаем все подряд, обрабатываем.
func (this *DelayedMessageService) Process() {
ticker := time.NewTicker(50 * time.Millisecond) // Очень оптимально
for range ticker.C {
// SELECT без блокировок - быстрее же
messages, _ := this.repo.GetDelayed()
for _, msg := range messages {
// Обрабатываем каждое сообщение
// Если другая горутина уже обрабатывает - ну и ладно
this.ProcessMessage(msg)
}
}
}
Что может пойти не так? Да похуй! MySQL справится. У нас же только 100k писем в день.
А если надо отменить задачу? Просто удаляем запись из таблицы. Между SELECT и обработкой проходит 2 мс — какие проблемы?
Борьба с дедлоками: креативный подход
У нас дедлоки. MySQL ругается, транзакции падают. Что делать?
Может, посмотрим SHOW ENGINE INNODB STATUS? Может, проверим порядок блокировок? Может, добавим индексы? Может, будем использовать транзакции, а не хуячить с двух коннектов SELECT/UPDATE/DELETE запросы?
Да нахуй. Это сложно.
Давайте лучше:
- Скопируем коннект к БД (старый “заблокирован”)
- Создадим локальный Redis-клиент для одной функции (глобальный “медленный”)
- Сделаем Redis-lock, чтобы избежать дедлока на БД (гениально!)
- Поставим рейтлимит на 5 запросов в минуту (нагрузка виновата)
// Оригинальный коннект "вызывал дедлоки"
db := this.dbConnectionManager.Get(spooler)
// Решение уровня бог
newDb := gorm.Open(mysql.Open(dbDsn), &gorm.Config{...})
// Или создаем локальный Redis для одной функции
localRedis := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
mutex := localRedis.NewMutex("my-special-lock")
Теперь у нас два пула соединений к одной БД, два Redis-клиента, и дедлоки как были, так и остались. Но зато мы что-то сделали!
А может, просто разобраться с причиной? Нет, это не для обезьян.
Помогите Даше-PHP-разработчику изучить GO
Этот код писали два PHPшника, которые решили, что освоили Go за полдня. И правда, синтаксис-то несложный! func, package, goroutine — все просто.
В PHP-проектах обычно либо полный пиздец (легаси, копипаста, алкоголизм), либо идеальный код (Symfony/Laravel с DDD). Золотой середины нет.
И когда PHP-разработчики приходят на Go, они приносят с собой:
thisв ресиверах — потому что привычно- 600 репозиториев для библиотек — будто тут composer есть
- Redis-lock для борьбы с дедлоками на БД — хз откуда они это нашли
- Отключенное форматирование — gofmt это для слабаков
- Заглушенные линтеры — они мешают писать код
- Тесты только на фронте — бэкенд работает, зачем тестить
- Невозможность поднять локально — надо тестить на внешнем сервере, как в лучшие времена PHP 5 + FTP. Сразу в прод!
- Добрую половину кода с any типом - нахер эту типизацию
Go дает инструменты для написания хорошего кода: типы, горутины, каналы, интерфейсы. Но если освоить язык за полдня, получается код, который работает хуже легаси PHP 5.3 без composer.
Самое ироничное, что они полностью уверены, что в их коде проблем нет. Проблема в “нагрузке”, “клиентах” и проч. Але. 400рпс это не нагрузка. Нет, бро. Проблема в том, что ты написал говно.
Инструкция по написанию говнокода
Если хотите написать код, от которого будут охуевать все, следуйте этим правилам:
- Блокируйте HTTP-обработчики на записи в каналы. Пользователь может подождать
- MySQL как очередь — селектим раз в 50мс, без транзакций. Race condition это теория
- Много пизди про инкапсуляцию, но backend пусть лезет в БД микросервисов напрямую. Быстрее же!
- Циклические зависимости — backend → БД спулера, спулер → HTTP backend. Замкнутый круг прекрасен
- Боритесь с симптомами — копируйте коннекты, создавайте Redis-lock, ставьте рейтлимиты. Только не разбирайтесь с причиной
- Забудьте про транзакции — они медленные. SELECT, потом UPDATE — так быстрее!
- Отключите линтеры и форматирование — они мешают творчеству
- Освойте язык за полдня и сразу в продакшн
- Тестируйте на проде — зачем нужен локальный env?
thisво всех ресиверах — ну ты понял- Вовремя съебись с проекта — желательно уйти на повышение
Вывод
Труд сделал из обезьяны человека, но некоторые обезьяны сильно не трудились и пошли писать на Go “хайлоад” с “инкапсуляцией”.
Если вы хотите писать хороший код на Go, рекомендую начать с изучения лучших практик и избегать описанных здесь антипаттернов. А если вам нужна помощь в автоматизации рутинных задач — попробуйте AI-редактор Cursor , который может помочь с рефакторингом и улучшением кода.
P.S. Все примеры кода взяты из реального production-проекта. Имена изменены, глупость осталась. Если вы узнали свой код — инкапсулируйте.