Представьте: три часа ночи, алерт на 47 ГБ RSS у сервиса, который «ничего не делает». Вы открываете pprof, видите 12 000 горутин — и понимаете, что цифра ничего не объясняет. go tool pprof показывает, где они живут, но не почему они там. Race detector выдаёт стектрейс из 80 строк, где две из них — ваши, а остальные — рантайм и сторонняя библиотека.
Именно в этот момент LLM из «игрушки для генерации boilerplate» превращается в напарника по диагностике — но только если вы даёте ей правильный вход. Не «найди утечку», а «вот вывод pprof, вот как устроен воркер, скажи, какой из четырёх паттернов здесь ломается». Ниже — 12 промтов, разбитых по уровню: от быстрого чтения симптома до проверки гипотез, которые вы бы сами проверяли час.
Базовый уровень: перевести симптом в гипотезу
1. Расшифровать дамп горутин в человеческие категории
Задача: у вас есть debug=1 дамп горутин, но 12 000 стеков сливаются в кашу. Нужно разбить их на паттерны, а не читать по одному.
Промт:
Ты — Go-инженер, специализирующийся на runtime и профилировании. Ниже — вывод `go tool pprof -traces` (или текстовый дамп `runtime.Stack(buf, true)`) из сервиса, у которого RSS растёт линейно и не падает после ночного пика.
Разбей все горутины на смысловые группы по стеку (не по количеству). Для каждой группы укажи:
1) где горутина «висит» (какой вызов блокирующий: select, chan recv, chan send, sync.WaitGroup.Wait, sync.Mutex.Lock, time.Sleep, netpoll);
2) растёт ли число горутин этой группы со временем и почему;
3) это утечка или нормальный пул (например, http.Server держит keep-alive соединения);
4) минимальный эксперимент, который подтвердит или опровергнет гипотезу за один запуск.
Не предлагай «добавить логирование» как первый шаг. Сначала — что уже видно в стеке.
[вставьте дамп]
Пример результата: LLM выделяет группу из 8 900 горутин, застрявших в chan send на буферизованном канале размера 100 — и объясняет, что продюсер пишет быстрее консьюмера, а консьюмер упал в context.WithTimeout, но не завершил цикл. Вы получаете не «где», а «почему».
chan send или chan recv — это почти всегда не утечка, а отсутствие backpressure. Начинайте не с кода, а с вопроса «кто должен был остановиться и не остановился».
2. Прочитать race-отчёт и отделить своё от чужого
Задача: race detector выдал отчёт, где фигурируют поля из вашего пакета и из encoding/json или ORM. Нужно понять, где реальная гонка, а где — следствие.
Промт:
Ниже — отчёт `go test -race` / `go run -race` для сервиса на Go 1.2x. Разбери его по шагам:
1) какие две горутины участвуют и какой адрес памяти общий;
2) какое поле или переменная реально гоночная (а не объект целиком);
3) какая из записей «первая» по времени и что это говорит о порядке инициализации;
4) отметь места, где стек уходит в стандартную библиотеку или сторонний пакет — там гонка обычно следствие, а не причина;
5) предложи минимальное исправление (мьютекс, атомик, канал, копирование значения) и объясни, почему остальные варианты здесь хуже.
Покажи, как выглядел бы отчёт, если бы исправление сработало (что именно исчезло бы из стека).
[вставьте отчёт]
Пример результата: модель указывает, что гонка идёт не на map, а на поле config.reloadAt типа time.Time, которое пишется из горутины-релоадера без синхронизации и читается в hot path. Исправление — atomic.Pointer[config] вместо мьютекса, потому что читателей много, писатель один.
3. Проверить контексты: кто не получил cancel
Задача: горутины живут дольше запроса, потому что context.WithCancel создали, но cancel() не вызвали на одной из веток.
Промт:
Проверь этот Go-код на утечки, связанные с context:
1) каждый ли `context.WithCancel` / `WithTimeout` / `WithDeadline` имеет гарантированный вызов cancel (включая пути с ошибкой и ранним return);
2) не передаётся ли `context.Background()` или `context.TODO()` в место, где должен быть родительский контекст запроса;
3) нет ли горутины, которая читает `ctx.Done()`, но не имеет выхода по другому условию (цикл без проверки ctx на каждой итерации);
4) где контекст хранится в структуре (это антипаттерн — объясни, чем он плох именно здесь).
Для каждой находки: строка, последствие в рантайме, исправление. Если проблем нет — скажи прямо, не выдумывай.
[вставьте код]
Пример результата: LLM находит ветку, где после if err != nil { return err } cancel не вызывается, а горутина-обработчик продолжает держать соединение с БД. Вы получаете точную строку и понимание, что таймер контекста продолжает тикать до дедлайна — при тысяче запросов в секунду это накапливается.
Продвинутый уровень: гипотезы и эксперименты
4. Смоделировать сценарий, при котором WaitGroup не досчитается
Задача: wg.Add вызывается внутри горутины, а не до её запуска — классика, которая «иногда работает».
Промт:
Вот функция, использующая sync.WaitGroup и каналы. Найди все места, где порядок Add/Done/Wait может привести к панике или зависанию. Для каждого:
1) опиши конкретный сценарий исполнения (какая горутина что делает в каком порядке), при котором происходит сбой;
2) объясни, почему тесты этого не ловят (обычно — из-за планировщика и GOMAXPROCS);
3) предложи исправление и покажи тест, который воспроизводит проблему стабильно (с `-count=100` или `-race`).
Не переписывай код целиком — покажи точечный дифф.
[вставьте код]
Пример результата: модель описывает гонку между wg.Add(1) внутри горутины и wg.Wait() в главной — и даёт тест с runtime.Gosched() в нужном месте, который падает в 9 из 10 прогонов. Это уже воспроизводимая проблема, а не «иногда падает на проде».
5. Найти закрытие канала, которого не должно быть
Задача: panic: send on closed channel в проде, но не в тестах. Обычно канал закрывает не тот, кто пишет.
Промт:
Проверь этот код на корректность работы с каналами:
1) кто владеет каналом (создаёт), кто пишет, кто читает, кто закрывает — построй таблицу;
2) есть ли путь, где канал закрывается дважды или закрывается писателем при нескольких продюсерах;
3) есть ли читатель, который продолжает слать в канал после close (например, в defer или в retry-цикле);
4) как выглядит корректный паттерн «один писатель, N читателей» и «N писателей, один читатель» для этого конкретного случая.
Для каждого нарушения покажи минимальный воспроизводящий пример.
[вставьте код]
Пример результата: LLM строит таблицу владения и показывает, что два воркера пишут в один канал, а закрывает его третий по таймауту. Исправление — sync.Once вокруг close или отдельный канал завершения.
6. Связать рост памяти с конкретным аллокатором
Задача: heap-профиль показывает рост, но неясно, это утечка объекта или просто GC не успевает.
Промт:
У меня есть `go tool pprof -http` heap-профиль (inuse_space) и график RSS. Помоги отличить утечку от давления GC:
1) какие признаки в heap-профиле говорят об утечке (растущий inuse_space в конкретной функции между двумя снимками), а какие — о нормальной работе;
2) как сравнить два профиля (`-base`) и что искать в diff;
3) какие типы объектов чаще всего «висят» из-за ссылок в глобальных структурах, кешах без TTL и замыканиях;
4) предложи команду `go tool pprof` и что в её выводе будет решающим.
Не говори «используйте pprof» — я уже его использую.
Пример результата: модель объясняет, что стабильный inuse_space при растущем RSS — это фрагментация или GOGC, а не утечка; и что diff двух профилей с интервалом в 10 минут покажет функцию, которая накопила объекты. Вы получаете критерий, а не ссылку на документацию.
7. Проверить пул соединений и его «невидимые» горутины
Задача: database/sql держит горутины, которые вы не создавали, и они маскируют вашу утечку.
Промт:
В дампе горутин есть группы из пакетов database/sql, net/http и стороннего клиента. Объясни:
1) какие горутины создаются этими библиотеками штатно и сколько их ожидается при N соединениях;
2) какие настройки пула (MaxOpenConns, MaxIdleConns, ConnMaxLifetime) влияют на число горутин и как;
3) как отличить «библиотека держит штатно» от «мой код не вернул соединение в пул»;
4) что искать в стеке, чтобы понять, что соединение не закрыто (например, отсутствие rows.Close, незакрытый tx).
[вставьте фрагмент дампа]
Пример результата: LLM показывает, что 40 горутин в database/sql.(*DB).connectionOpener — норма при MaxIdleConns=40, а 900 горутин в (*Rows).Next — это забытый rows.Close() в обработчике ошибки.
Экспертный уровень: заставить LLM спорить с вами
8. Сгенерировать тест, который ловит утечку за секунды
Задача: воспроизвести утечку в CI, а не ждать продакшена.
Промт:
Напиши Go-тест, который стабильно обнаруживает утечку горутин в этой функции. Требования:
1) используй `runtime.NumGoroutine()` или `goleak` (uber-go/goleak) — объясни, какой вариант надёжнее и почему;
2) тест должен падать на текущей реализации и проходить после исправления, которое ты предложишь;
3) учти, что фоновые горутины рантайма и тестового фреймворка не должны давать ложных срабатываний;
4) покажи вывод теста при падении — что именно увидит разработчик в CI.
[вставьте функцию]
Пример результата: тест с goleak.VerifyNone(t) и явным time.Sleep для планировщика, который падает с сообщением «found unexpected goroutines» и стеком виновника. Плюс замечание: NumGoroutine() без baseline даёт ложные срабатывания в параллельных тестах.
9. Проверить гипотезу «это не наш код, а библиотека»
Задача: подозрение, что утечка в стороннем пакете, но доказать это сложно.
Промт:
Я подозреваю, что утечка горутин идёт из сторонней библиотеки, а не из моего кода. Помоги построить доказательство:
1) как изолировать библиотеку в отдельном тесте и измерить число горутин до и после;
2) какие признаки в стеке указывают на библиотеку (имя пакета, сигнатура функции, замыкание);
3) как проверить версию библиотеки и найти, исправлено ли это в более новой (что искать в changelog и issues);
4) если утечка подтверждена — какие обходные пути есть без форка (обёртка, ограничение параллелизма, явное закрытие).
Не утверждай, что это баг библиотеки, пока не показан воспроизводящий тест.
Пример результата: модель предлагает минимальный main, который подключает библиотеку, делает один вызов и печатает runtime.NumGoroutine() до и после с time.Sleep(2*time.Second). Если число растёт — доказательство есть, и вы идёте в issues с готовым репро.
10. Разобрать дедлок по двум мьютексам
Задача: сервис завис, SIGQUIT дал дамп, где все горутины спят на sync.Mutex.Lock.
Промт:
Ниже — дамп горутин, снятый по SIGQUIT во время зависания. Найди цикл ожидания:
1) построй граф «горутина → мьютекс, который она держит → мьютекс, которого ждёт»;
2) покажи цикл, если он есть, и укажи порядок захвата в каждом месте;
3) объясни, как этот дедлок мог не проявляться в тестах (разный порядок захвата в двух путях);
4) предложи правило порядка захвата и как его проверить (например, единая функция, которая всегда берёт A, потом B).
Если цикла нет — скажи, что это не дедлок, а starvation или блокировка на канале.
Пример результата: граф показывает цикл A→B в одном обработчике и B→A в другом; модель предлагает вынести захват в одну функцию и добавляет в тест параллельный вызов обоих путей с -race.
11. Спроектировать нагрузочный эксперимент, который воспроизводит race
Задача: гонка есть, но ловится раз в сутки. Нужно ускорить её проявление.
Промт:
У меня есть подозрение на гонку между двумя горутинами, но `-race` в обычных тестах её не ловит. Предложи эксперимент:
1) как увеличить вероятность гонки (GOMAXPROCS, число итераций, `-count`, `runtime.Gosched`, задержки в критических местах);
2) как использовать `-race` в сочетании с нагрузкой, не убив CI по времени;
3) какие метрики снимать, чтобы понять, что гонка воспроизвелась (не только падение);
4) как оформить это как отдельный «медленный» тест, который не блокирует основной пайплайн.
Дай конкретные флаги и структуру теста, а не общие советы.
Пример результата: модель предлагает тест с t.Parallel(), -race -count=200, runtime.GOMAXPROCS(4) и искусственной задержкой между read и write через time.Sleep(time.Microsecond). Race detector срабатывает в большинстве прогонов, тест помечен //go:build race и идёт отдельным job в CI.
12. Проверить архитектуру на устойчивость к утечкам до релиза
Задача: не ловить утечки в проде, а спроектировать так, чтобы их было видно сразу.
Промт:
Вот архитектура сервиса (опишу словами): HTTP-хендлеры, пул воркеров, фоновая горутина-релоадер конфига, кеш с TTL, клиент к внешнему API. Проведи аудит на устойчивость к утечкам горутин и гонкам:
1) для каждого компонента — какие горутины он создаёт и кто их завершает;
2) где нет явного владельца жизненного цикла (кто должен вызвать cancel/close);
3) какие метрики и проверки добавить, чтобы утечка была видна в первые минуты (число горутин, размер очередей, время ожидания в каналах);
4) какие два теста-«стража» стоит добавить в CI, чтобы регрессия не прошла.
Дай таблицу «компонент → риск → как заметить → как исправить». Без общих слов про «лучшие практики».
Пример результата: таблица, где у кеша с TTL нет фоновой очистки — записи живут вечно; у релоадера нет выхода по контексту — он переживает shutdown; у воркеров нет backpressure — очередь растёт. И два теста: goleak на graceful shutdown и проверка, что после отмены контекста число горутин вернулось к baseline.
Что реально даёт LLM в диагностике, а что нет
Trade-off честный. LLM отлично читает артефакты: дампы, отчёты, профили, стектрейсы — и находит паттерны быстрее, чем человек с 80 строками race-отчёта в терминале. Но она не запускает ваш код и не знает вашу архитектуру, пока вы её не опишете. Если дать ей только «найди утечку» без данных — получите список банальностей про закрытие каналов.
Режим отказа тоже предсказуем: модель уверенно называет причиной то, что видно в стеке, но стек показывает место блокировки, а не логическую ошибку, которая к ней привела. Поэтому последний шаг всегда за вами — воспроизвести в тесте. Промты 8–11 как раз про это: превратить гипотезу в падающий тест.
И неочевидное: самые полезные промты — не «найди проблему», а «построй граф», «построй таблицу владения», «покажи, как выглядел бы отчёт после исправления». Структурированный вывод заставляет модель опираться на данные, а вам даёт артефакт, который можно показать команде на ревью.
Начните с одного: возьмите последний дамп горутин из прода и прогоните через промт №1. Если после ответа у вас появилась гипотеза, которую можно проверить тестом за 15 минут, — подход работает. Если нет — добавьте в промт данных: конкретный стек, настройки пула, версию Go. Диагностика начинается не с вопроса «что не так», а с вопроса «вот что я вижу — что это значит».