Промты для Rust и Go: как в одиночку тянуть высоконагруженный бэкенд, пока команда из трёх разработчиков тонет в созвонах
В сентябре 2026-го я закрывал проект, на который по нормальным прикидкам нужна была команда из трёх человек: gRPC-фасад, два микросервиса на Go, один latency-критичный воркер на Rust, плюс профилирование и ревью. Со мной был только один союзник — LLM. И это не история про «ИИ написал за меня весь код». Это история про то, как правильно сформулированный промт заменяет тимлида, который держит в голове контекст всей системы.
Разница между «ИИ пишет мусор» и «ИИ экономит недели» — почти всегда в постановке задачи. Если попросить «сделай мне сервис на Go», получишь учебный пример из туториала. Если дать модели точный контракт: версию Go, протокол, бюджет по памяти, требования к отмене контекста — получишь рабочий скелет, который останется только довести. Ниже — 10 промтов, которые я реально использовал в этом проекте, с примерами кода и разбором, почему формулировка работает.
1. Промт для старта: спроектируй gRPC-сервис из спецификации
Первый и главный промт. Он превращает .proto-файл в структуру проекта, а не в кашу из файлов.
Промт:
«Ты — Go-разработчик уровня senior. Ниже .proto-контракт. Спроектируй структуру сервиса на Go 1.24: слои (transport / service / repository), где живут интерсепторы, как организуется graceful shutdown. Не пиши бизнес-логику — только скелет с TODO и обоснованием каждого файла. Верни дерево каталогов и содержимое ключевых файлов.»
Пример контракта:
syntax = "proto3";
package orders.v1;
service OrderService {
rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse);
rpc GetOrder(GetOrderRequest) returns (GetOrderResponse);
}
Модель выдаёт дерево вида cmd/server, internal/order, internal/transport/grpc, internal/repo/postgres и отдельно — файл с graceful shutdown через signal.NotifyContext. Ключевой момент: просьба обосновать каждый файл заставляет модель не выдумывать лишние абстракции. Один и тот же промт стабильно даёт структуру, которую не стыдно показать на код-ревью.
2. Промт для конкурентности: семафоры и errgroup без гонок
Асинхронное программирование — место, где LLM чаще всего врёт. Лечится явным требованием указать модель памяти и способ синхронизации.
Промт:
«Напиши на Go функцию, которая параллельно дёргает N внешних HTTP-вызовов с лимитом конкурентности K. Обязательно:
errgroup.GroupсSetLimit, отмена черезcontext.WithCancel, сбор ошибок без потери первой. После кода — объясни, почему здесь достаточноerrgroup, и в каком случае понадобился быsync.Mutexили канал-семафор.»
func fanOut(ctx context.Context, urls []string, k int) ([]string, error) {
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(k)
out := make([]string, len(urls))
for i, u := range urls {
i, u := i, u
g.Go(func() error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
out[i] = string(b)
return nil
})
}
return out, g.Wait()
}
Требование «объясни, когда нужен Mutex» — это бесплатное мини-ревью. Модель почти всегда добавляет предупреждение про захват переменной цикла (актуально до Go 1.22, где семантика изменилась) и про то, что errgroup.SetLimit появился в Go 1.20.
3. Промт для Rust: Send + Sync и почему компилятор ругается
Rust-воркер у меня держал соединения к брокеру и считал агрегаты. Первое, что делает LLM без подсказки, — пихает Arc<Mutex<...>> везде, где видит шаринг. Промт это лечит.
Промт:
«Ниже структура на Rust с tokio. Объясни, почему компилятор требует Send + Sync для задач в tokio::spawn. Перепиши так, чтобы избежать Arc
, если состояние можно сделать иммутабельным. Покажи оба варианта и назови trade-off по contention.»
Показательный фрагмент, который модель выдаёт после такого промта:
use std::sync::Arc;
use tokio::sync::mpsc;
pub struct Worker {
tx: mpsc::Sender<Job>,
}
impl Worker {
pub fn spawn(buffer: usize) -> Arc<Self> {
let (tx, mut rx) = mpsc::channel::<Job>(buffer);
tokio::spawn(async move {
while let Some(job) = rx.recv().await {
process(job).await;
}
});
Arc::new(Self { tx })
}
}
Главная ценность промта — не код, а раздел «trade-off». Модель объясняет, что канал-очередь снижает contention по сравнению с Mutex на горячем пути, но добавляет аллокации и backpressure. Это ровно тот разговор, который вы бы вели с сеньором на архитектурной сессии.
4. Промт для профилирования: pprof и flamegraph по симптомам
«Сервис тормозит» — бесполезный запрос. Работает такой:
Промт:
«Go-сервис на 12k RPS показывает p99 = 340ms при p50 = 8ms. Подозреваю GC или блокировки на мьютексе. Составь пошаговый план профилирования через net/http/pprof и go tool pprof: какие именно профили снимать (cpu, heap, mutex, block), что искать в каждом, какие команды запускать. Дай список из 5 гипотез с признаком в профиле для каждой.»
Модель выдаёт конкретику: go tool pprof -http=:8080 http://localhost:6060/debug/pprof/profile?seconds=30, затем go tool pprof -top, поиск runtime.gcBgMarkWorker в CPU-профиле как признак GC-давления, sync.(*Mutex).Lock в block-профиле как признак contention. Отдельно — гипотезу про json.Marshal на горячем пути. Это не «магия ИИ», это сжатый конспект официальной документации по pprof, применённый к моему симптому.
5. Промт для бенчмарков: сравни две реализации честно
Когда у меня было два варианта парсера, я не стал писать бенчмарк руками.
Промт:
«Напиши бенчмарк на Go для двух реализаций функции Parse. Используй testing.B, sub-benchmarks, b.ReportAllocs() и b.ResetTimer(). Обе реализации должны быть в одном файле. После кода объясни, почему b.N не фиксируется и как избежать оптимизации компилятором через sink-переменную.»
var sink any
func BenchmarkParse(b *testing.B) {
for _, tc := range []struct{ name string; fn func([]byte) (any, error) }{
{"stdlib", ParseStdlib},
{"custom", ParseCustom},
} {
b.Run(tc.name, func(b *testing.B) {
data := []byte(`{"order_id":"42","amount":1990}`)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v, err := tc.fn(data)
if err != nil { b.Fatal(err) }
sink = v
}
})
}
}
b.ReportAllocs() и sink-переменная — это то, что отличает настоящий бенчмарк от игрушки. Модель знает про это только если вы явно попросите.
6. Промт для ревью: найди data race до продакшена
Этот промт я гонял на каждом PR. Он дешёвый и ловит то, что глазами не видно.
Промт:
«Проведи ревью кода ниже как инженер, который ищет только три класса проблем: data race, утечки горутин, необработанные ошибки. Для каждой находки укажи строку, почему это проблема и как исправить. Если проблем нет — так и скажи, не выдумывай.»
Последняя фраза критична. Без неё модель «находит» проблемы там, где их нет, — классический sycophancy. С ней ревью становится пригодным: из десяти PR она реально указывала на забытый defer resp.Body.Close() и на горутину, которая писала в канал без закрытия.
7. Промт для отмены контекста: сквозной context.Context
В микросервисах отмена запроса — источник утечек. Промт для генерации сквозного контекста:
Промт:
«Покажи, как правильно протянуть context.Context от gRPC-хендлера до SQL-запроса в слое repository. Что произойдёт с запросом в Postgres, если клиент отменит вызов? Как проверить это в тесте? Дай код хендлера, репозитория и тест с context.WithTimeout.»
Модель объясняет, что драйвер pgx поддерживает отмену через ctx, и что без проброса контекста запрос продолжит выполняться на сервере даже после разрыва соединения. Тест с context.WithTimeout(ctx, 10*time.Millisecond) и проверкой errors.Is(err, context.DeadlineExceeded) закрывает вопрос.
8. Промт для gRPC-интерсепторов: метрики и трейсинг без боли
Промт:
«Напиши unary-интерсептор на Go, который логирует метод, длительность и код ошибки, и добавляет trace-id из metadata. Покажи, как подключить его в grpc.NewServer. Отдельно — как сделать то же самое в Go 1.24 через новый пакет log/slog, без сторонних логгеров.»
func UnaryLogger(log *slog.Logger) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, h grpc.UnaryHandler) (any, error) {
start := time.Now()
resp, err := h(ctx, req)
code := status.Code(err)
log.Info("rpc",
slog.String("method", info.FullMethod),
slog.Duration("dur", time.Since(start)),
slog.String("code", code.String()),
)
return resp, err
}
}
Промт ценен тем, что заставляет модель использовать актуальный log/slog (появился в Go 1.21) вместо устаревших логгеров, и не тащить лишние зависимости.
9. Промт для Rust: async без блокирующих вызовов
Классическая ошибка — вызвать блокирующий код внутри async fn. Промт:
Промт:
«Найди в коде ниже блокирующие вызовы внутри async-функций tokio. Для каждого предложи замену: spawn_blocking или асинхронный аналог. Объясни, почему блокировка runtime-потока опаснее в tokio, чем в потоках ОС. Покажи до и после.»
Модель находит std::fs::read_to_string внутри async fn и меняет на tokio::fs::read_to_string, а для CPU-тяжёлой работы — на tokio::task::spawn_blocking. Объяснение про work-stealing планировщик и starvation — точное и без воды.
10. Промт для документации: ADR из diff'а
Последний, но не по важности. Архитектурные решения забываются через месяц.
Промт:
«Ниже git diff за последнюю неделю. Составь Architecture Decision Record в формате: контекст, решение, альтернативы, последствия. Язык — сухой, без маркетинга. Если из diff'а решение не выводится однозначно — задай мне уточняющий вопрос.»
Последнее предложение превращает промт в диалог. Модель спрашивает: «Почему вы выбрали pgx вместо database/sql — производительность или фичи?» — и это ровно тот вопрос, который я бы забыл записать в ADR.
Что реально даёт этот подход
| Задача | Без промтов | С промтами |
|---|---|---|
| Скелет gRPC-сервиса | 1–2 дня | 2–3 часа |
| Поиск data race в ревью | по остаточному принципу | на каждом PR |
| Профилирование p99 | полдня на гипотезы | 1 час по чек-листу |
| ADR по изменениям | «потом напишу» | генерируется из diff |
Цифры — из моего проекта, не универсальная статистика. Но паттерн устойчивый: LLM не заменяет инженера, она заменяет того второго и третьего разработчика, которых вы бы позвали на созвон, чтобы обсудить структуру пакетов. Разница только в том, что созвон стоит час всем, а промт — минуту вам.
Если хотите попробовать — начните с промта №1 на своём реальном .proto, а не на учебном. Именно на реальном контракте модель показывает, умеет ли она думать в вашем контексте, или просто пересказывает туториал. И держите под рукой официальные источники: Go Concurrency Patterns, Rust Async Book, gRPC Go docs — модель иногда ошибается, и проверять её по первоисточникам полезно и вам, и ей.
Комментарии