Goroutines, pprof, gRPC: 12 промптов, с которыми мой Go-бэкенд перестал падать в проде

Goroutines, pprof, gRPC: 12 промптов, с которыми мой Go-бэкенд перестал падать в проде

Go — язык, где всё выглядит просто ровно до момента, когда в проде утекает память из-за незакрытого context, а go vet молчит. Лично я перестал гуглить «why is my goroutine leak» после того, как собрал набор промптов, которые закрывают 90 % типовых задач: от генерации идиоматичного кода до разбора heap-профиля и подготовки манифестов в Kubernetes.

Промпт — это не «сделай за меня». Это способ заставить модель думать в парадигме Go: с каналами вместо мьютексов, с errgroup вместо WaitGroup, с sync.Pool вместо аллокаций в горячем пути. В каждом сценарии ниже я даю конкретную задачу, готовый текст запроса и объясняю, что именно он решает в реальном проде. Ссылки на официальную документацию прилагаю — чтобы вы не верили мне на слово.

1. Идиоматичный код вместо «Java на Go»

Задача: новичок пишет if err != nil { return err } через 5 строк и называет структуры UserManager.

Промпт:

Ты — ревьюер Go-кода уровня production. Перепиши функцию ниже в идиоматичном стиле:
- ошибки оборачивай через fmt.Errorf с %w (см. https://go.dev/blog/go1.13-errors)
- не используй префикс Get для геттеров (https://go.dev/doc/effective_go#Getters)
- интерфейсы объявляй на стороне потребителя, а не реализации
- избегай stutter: user.UserService -> user.Service
Верни diff и объясни каждое изменение одной строкой.

Что решает: ревью в стиле Effective Go за секунды. В проде это снижает количество споров на code review и ускоряет онбординг. Официальный гайд — go.dev/doc/effective_go.

2. Горутины и каналы: fan-in/fan-out без утечек

Задача: нужно распараллелить вызовы к 1000 URL-ов, но процессор не должен уйти в OOM.

Промпт:

Напиши на Go паттерн fan-out/fan-in с ограничением конкурентности через семафор-канал.
Требования:
- контекст с отменой прокидывается во все воркеры
- producer закрывает канал, consumer использует range
- ошибки собираются через golang.org/x/sync/errgroup
- покажи вариант с worker pool и объясни, почему закрытие канала — обязанность producer
Приведи пример с 3 воркерами на 10 задач и вывод в stdout.

Что решает: классическая ошибка — fatal error: all goroutines are asleep - deadlock!. Промпт даёт рабочий шаблон, который я потом копирую в 80 % сервисов. Официально про паттерн каналов: go.dev/tour/concurrency.

3. Оптимизация памяти и escape-анализ

Задача: аллокации в горячем пути дают 40 % времени в GC.

Промпт:

Проанализируй функцию и предложи оптимизации аллокаций:
- что уходит в heap по escape-анализу (go build -gcflags=-m)
- где заменить []byte на sync.Pool
- где использовать strings.Builder вместо конкатенации
- где применим strconv.AppendInt вместо strconv.Itoa
Дай код до/после и команду для замера: go test -bench=. -benchmem

Что решает: -benchmem показывает allocs/op — метрика, которую я гоняю в CI. Уменьшение аллокаций напрямую снижает нагрузку на GC. Документация по escape-анализу — go.dev/doc/gc-guide.

4. Тесты и бенчмарки с table-driven подходом

Задача: покрытие 0 %, а дедлайн вчера.

Промпт:

Сгенерируй table-driven тесты для функции ParseConfig на Go.
- используй t.Run для подтестов
- добавь t.Parallel() где безопасно
- для бенчмарка используй b.ResetTimer() и b.ReportAllocs()
- добавь пример с testing/fstest для файловых операций
Покажи, как запускать только один подтест: go test -run TestParseConfig/empty_input

Что решает: table-driven тесты — стандарт в Go (см. go.dev/wiki/TableDrivenTests). Промпт экономит час на бойлерплейте.

5. Проектирование gRPC-сервиса

Задача: нужен контракт между тремя командами без ручной синхронизации.

Промпт:

Спроектируй .proto для gRPC-сервиса заказов. Требования:
- proto3, package с версионированием (order.v1)
- используй google.protobuf.Timestamp и well-known types
- поля с резервированием: reserved 4, 5; reserved "old_field";
- добавь streaming-метод для подписки на статус заказа
- сгенерируй серверный интерфейс и покажи, как подключить grpc-gateway для REST
Объясни, почему нельзя переиспользовать номера полей.

Что решает: reserved — защита от breaking changes, о которой забывают. Официальный гайд: protobuf.dev/programming-guides/proto3.

6. Перехватчики (interceptors) для логирования и метрик

Задача: сквозной request-id и метрики Prometheus во всех методах.

Промпт:

Напиши unary и stream interceptor на Go для gRPC:
- unary: логирование с request-id из metadata, замер latency
- stream: обёртка ServerStream с переопределением RecvMsg/SendMsg
- метрики через github.com/prometheus/client_golang: histogram по методу
Покажи регистрацию через grpc.ChainUnaryInterceptor и порядок вызовов.

Что решает: interceptor — единственное правильное место для cross-cutting concerns. Документация: grpc.io/docs/guides/interceptors.

7. Context: отмена и таймауты без утечек

Задача: запрос зависает навсегда, потому что context.Background() внутри.

Промпт:

Проверь код на правильное использование context:
- context должен быть первым аргументом и называться ctx
- не храни context в структуре (go.dev/blog/context-and-structs)
- используй context.WithTimeout вместо time.After
- покажи, как context.Value убивает типизацию и где вместо него передать параметр
Перепиши функцию с context.WithCancel и defer cancel().

Что решает: утечка горутин через context — топ-1 причина роста памяти. Разбор от Go-команды: go.dev/blog/context.

8. Отладка утечек горутин через pprof

Задача: количество горутин растёт монотонно, RSS пухнет.

Промпт:

Объясни пошагово, как найти утечку горутин:
1. подключить net/http/pprof и снимать /debug/pprof/goroutine?debug=2
2. сравнить два снимка: go tool pprof -base old.prof new.prof
3. читать вывод top и list по функции
4. найти горутину, которая висит на <-ch или select без default
Дай реальный пример кода с утечкой и исправленную версию.

Что решает: pprof — встроенный профайлер, не нужно ничего ставить. Официально: pkg.go.dev/net/http/pprof.

9. Профилирование CPU и heap

Задача: p99 latency вырос с 50 мс до 500 мс после релиза.

Промпт:

Помоги разобрать CPU-профиль:
- go tool pprof -http=:8080 cpu.prof — на что смотреть в Flame Graph
- как отличить runtime.mallocgc от реальной бизнес-логики
- для heap: go tool pprof -alloc_space vs -inuse_space, в чём разница
- как снять профиль в проде безопасно: runtime/pprof с ручным триггером
Дай чек-лист из 5 пунктов для регулярного профилирования.

Что решает: -alloc_space показывает все аллокации, -inuse_space — что реально живёт. Разница критична для оптимизации. Гайд: go.dev/blog/pprof.

10. Dockerfile для Go: multi-stage и статическая сборка

Задача: образ весит 900 МБ и содержит компилятор.

Промпт:

Напиши multi-stage Dockerfile для Go-сервиса:
- builder: golang:1.23-alpine, CGO_ENABLED=0, флаги -ldflags="-s -w"
- runtime: gcr.io/distroless/static-debian12 или scratch
- скопируй только бинарник и CA-сертификаты
- USER nonroot:nonroot
- HEALTHCHECK или отдельный /healthz эндпоинт
Объясни, зачем -s -w и почему CGO_ENABLED=0 важен для scratch.

Что решает: финальный образ ~10-15 МБ вместо сотен. -s -w убирает debug-символы. Документация: docs.docker.com/build/building/multi-stage.

11. Kubernetes-манифесты с probes и limits

Задача: под падает по OOMKilled, readiness не настроен.

Промпт:

Сгенерируй Deployment + Service + HPA для Go-сервиса:
- livenessProbe и readinessProbe на /healthz с разными порогами
- resources.requests и limits по CPU/памяти (объясни, почему requests влияет на scheduling)
- graceful shutdown: terminationGracePeriodSeconds + обработка SIGTERM в main
- PDB для минимального количества доступных подов
Покажи код main.go с signal.NotifyContext для корректного shutdown.

Что решает: SIGTERM + signal.NotifyContext — правильный graceful shutdown, без обрыва активных запросов. Документация: kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle.

12. Ревью SQL и работа с database/sql

Задача: N+1 запрос в цикле и sql.Rows без defer Close().

Промпт:

Проверь работу с database/sql:
- всегда defer rows.Close() сразу после Query
- проверяй rows.Err() после цикла
- используй PrepareContext для повторяющихся запросов
- покажи, как заменить N+1 на JOIN или batch через ANY($1) в PostgreSQL
- настрой SetMaxOpenConns и SetConnMaxLifetime — объясни, как это влияет на pgbouncer

Что решает: забытый rows.Close() держит соединение из пула. Лимиты пула критичны при работе через PgBouncer. Документация: pkg.go.dev/database/sql.

Как использовать эти промпты, чтобы не выстрелить себе в ногу

Главное правило: всегда проси объяснить и всегда проверяй код. LLM отлично знает синтаксис Go, но не знает вашу бизнес-логику и версии зависимостей. Прогоняйте сгенерированный код через go vet, staticcheck и golangci-lint, а бенчмарки — через go test -bench=. -benchmem. Документация Go — источник истины, а промпт — ускоритель, а не замена мышления.

Соберите эти 12 сценариев в свой личный «промпт-пак» — в файл, в сниппеты IDE, в шаблоны команды. Через месяц вы заметите, что тратите время на архитектуру и профилирование, а не на бойлерплейт и гугление сигнатур. Начните с одного промпта — того, где сегодня болит сильнее всего.

← All posts

Comments