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

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

Антипаттерны Go: реальные примеры плохого кода из production. Блокирующие каналы в HTTP-обработчиках, race conditions, дедлоки, нарушение инкапсуляции. Как не писать код на Go.

Современные разработчики часто пишут на 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
T1SELECT job_id=42 WHERE status=new
T2SELECT job_id=42 WHERE status=new
T3UPDATE job_id=42 SET status=in_progress
T4UPDATE 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 запросы?

Да нахуй. Это сложно.

Давайте лучше:

  1. Скопируем коннект к БД (старый “заблокирован”)
  2. Создадим локальный Redis-клиент для одной функции (глобальный “медленный”)
  3. Сделаем Redis-lock, чтобы избежать дедлока на БД (гениально!)
  4. Поставим рейтлимит на 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рпс это не нагрузка. Нет, бро. Проблема в том, что ты написал говно.

Инструкция по написанию говнокода

Если хотите написать код, от которого будут охуевать все, следуйте этим правилам:

  1. Блокируйте HTTP-обработчики на записи в каналы. Пользователь может подождать
  2. MySQL как очередь — селектим раз в 50мс, без транзакций. Race condition это теория
  3. Много пизди про инкапсуляцию, но backend пусть лезет в БД микросервисов напрямую. Быстрее же!
  4. Циклические зависимости — backend → БД спулера, спулер → HTTP backend. Замкнутый круг прекрасен
  5. Боритесь с симптомами — копируйте коннекты, создавайте Redis-lock, ставьте рейтлимиты. Только не разбирайтесь с причиной
  6. Забудьте про транзакции — они медленные. SELECT, потом UPDATE — так быстрее!
  7. Отключите линтеры и форматирование — они мешают творчеству
  8. Освойте язык за полдня и сразу в продакшн
  9. Тестируйте на проде — зачем нужен локальный env?
  10. this во всех ресиверах — ну ты понял
  11. Вовремя съебись с проекта — желательно уйти на повышение

Вывод

Труд сделал из обезьяны человека, но некоторые обезьяны сильно не трудились и пошли писать на Go “хайлоад” с “инкапсуляцией”.

Если вы хотите писать хороший код на Go, рекомендую начать с изучения лучших практик и избегать описанных здесь антипаттернов. А если вам нужна помощь в автоматизации рутинных задач — попробуйте AI-редактор Cursor , который может помочь с рефакторингом и улучшением кода.


P.S. Все примеры кода взяты из реального production-проекта. Имена изменены, глупость осталась. Если вы узнали свой код — инкапсулируйте.