Go без сюрпризов: 12 промтов для генерации конкурентного кода, gRPC-сервисов и оптимизации горутин
Go создавался в Google для сетевых сервисов, и его модель конкурентности — каналы и горутины — стала визитной карточкой языка. Но именно здесь прячутся самые дорогие баги: deadlock'и, race conditions, утечки горутин и context canceled, которые всплывают только под нагрузкой. По данным опроса Go Developer Survey (go.dev/blog/survey2023-h2-results), разработчики регулярно называют конкурентность одной из самых сложных тем языка.
Хорошая новость: LLM уже неплохо знает идиомы Go. Плохая — без точного промта она выдаёт «работающий» код, который падает на ревью или в продакшене. Ниже — 12 промтов, которые я использую каждый день: от каналов и errgroup до pprof-профилирования и бенчмарков. Каждый промт — с задачей, готовым текстом и пояснением, когда его применять.
1. Идиоматичный конкурентный код с каналами и sync
Когда применять: нужен воркер-пул, fan-out/fan-in или pipeline без утечек горутин.
Промт:
Ты — senior Go-разработчик. Сгенерируй идиоматичный код для задачи: [опиши задачу]. Требования: используй
context.Contextдля отмены,sync.WaitGroupдля ожидания, каналы буферизованные там, где это оправдано. Закрывай каналы только со стороны отправителя. Объясни, где может возникнуть deadlock и как ты его избежал.
Пример: воркер-пул на 5 горутин, читающий URL из канала и пишущий результат в results.
func workerPool(ctx context.Context, urls <-chan string, workers int) <-chan string {
results := make(chan string, workers)
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for url := range urls {
select {
case <-ctx.Done():
return
case results <- fetch(url):
}
}
}()
}
go func() { wg.Wait(); close(results) }()
return results
}
2. Ревью на race conditions и утечки горутин
Когда применять: перед мержем кода, который трогает общее состояние.
Промт:
Проведи ревью кода ниже на предмет race conditions, утечек горутин и deadlock'ов. Для каждой проблемы укажи строку, объясни механизм и предложи минимальный фикс. Учти правила go vet,
-raceи рекомендации из Go Memory Model (go.dev/ref/mem).
Пример проблемы: горутина пишет в map без мьютекса — go test -race это поймает, но ревью быстрее.
// ПЛОХО: concurrent map writes
var m = map[string]int{}
for _, v := range vals { go func(){ m[v]++ }() }
// ХОРОШО:
var mu sync.Mutex
for _, v := range vals {
go func(v string){ mu.Lock(); m[v]++; mu.Unlock() }(v)
}
Всегда передавай переменную цикла параметром — в Go до 1.22 замыкание захватывало её по ссылке (см. release notes Go 1.22, go.dev/doc/go1.22).
3. gRPC-сервис из proto-файла
Когда применять: нужен boilerplate для нового сервиса.
Промт:
По .proto ниже сгенерируй полный gRPC-сервер на Go: реализацию сервиса, interceptor'ы для логирования и recovery, graceful shutdown через
grpc.GracefulStop(), а также клиент с таймаутами. Используйgoogle.golang.org/grpcиprotoc-gen-go-grpc. Не забудь проcontextв каждом методе.
Пример: сервис UserService с методом GetUser.
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
u, err := s.db.Find(ctx, req.Id)
if err != nil { return nil, status.Errorf(codes.NotFound, "user %s: %v", req.Id, err) }
return u, nil
}
4. Оптимизация горячего пути по pprof
Когда применять: сервис жрёт CPU или память, но непонятно где.
Промт:
У меня есть вывод
go tool pprof -top(вставь). Объясни топ-5 функций по flat/cum, предложи конкретные оптимизации: аллокации, sync.Pool, замена структур, уменьшение escape-анализа. Для каждой гипотезы дай код до/после и способ проверить через-benchmem.
Пример: fmt.Sprintf в горячем пути заменяем на strconv + strings.Builder — типичный выигрыш по аллокациям.
5. Бенчмарки и таблица результатов
Когда применять: нужно доказать, что оптимизация работает.
Промт:
Напиши бенчмарки Go для функции [X] в стиле table-driven. Используй
b.ReportAllocs(),b.ResetTimer(), sub-benchmarks черезb.Run. Сравни минимум 3 реализации и объясни, как читатьns/op,B/op,allocs/op.
func BenchmarkParse(b *testing.B) {
for _, impl := range []struct{name string; fn func(string) int}{
{"naive", parseNaive}, {"optimized", parseOpt},
} {
b.Run(impl.name, func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ { impl.fn(sample) }
})
}
}
6. errgroup вместо ручного WaitGroup
Когда применять: несколько параллельных задач, где важна первая ошибка.
Промт:
Перепиши код с
sync.WaitGroupнаgolang.org/x/sync/errgroupсerrgroup.WithContext. Покажи отмену остальных горутин при первой ошибке и установкуSetLimit.
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(10)
for _, id := range ids {
id := id
g.Go(func() error { return process(ctx, id) })
}
return g.Wait()
7. Worker pool с backpressure
Когда применять: неконтролируемый рост очереди задач.
Промт:
Сгенерируй worker pool с ограниченным буфером канала и backpressure: если очередь заполнена, продюсер блокируется или отбрасывает задачи с метрикой. Объясни trade-off между блокировкой и drop.
8. Graceful shutdown HTTP-сервера
Когда применять: деплой без обрыва соединений.
Промт:
Напиши main с graceful shutdown:
signal.NotifyContext,http.Server.Shutdownс таймаутом, ожидание активных запросов, закрытие БД. Учти порядок остановки.
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
<-ctx.Done()
shCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = srv.Shutdown(shCtx)
9. Пайплайны и select с default
Когда применять: нужно неблокирующее чтение или таймауты.
Промт:
Объясни и покажи паттерны
select: неблокирующийdefault,time.Afterдля таймаута, приоритетныйselectчерез вложенный. Покажи, почемуtime.Afterв цикле течёт — замени наtime.NewTimer.
10. Контекст: отмена, дедлайны, значения
Когда применять: везде, где есть I/O.
Промт:
Покажи правила работы с
context: где хранить, а где не хранить значения, почему не передаватьcontext.Background()вглубь, как правильно деривировать дедлайны. Приведи антипаттерны.
11. Тесты с -race и t.Parallel
Когда применять: конкурентный код без покрытия.
Промт:
Напиши тесты для функции [X], включая конкурентный кейс с
t.Parallel(). Покажи, как запускатьgo test -race -count=10и почему-countважен для флаки-тестов.
12. Генерация кода: go generate и stringer
Когда применять: повторяющийся boilerplate.
Промт:
Покажи, как настроить
go generateсstringerиmockgenдля проекта. Приведи директиву в коде, Makefile-цель и объясни порядок в CI.
//go:generate stringer -type=Status
type Status int
Что дальше
Эти 12 промтов покрывают 80% ежедневной работы Go-разработчика: конкурентность, gRPC, профилирование и тесты. Главное правило — всегда просить модель обосновать решение и показать, как проверить результат (-race, pprof, -benchmem). LLM ускоряет черновик, но ответственность за deadlock остаётся на вас.
Начните с промта №1 и №2 на текущем проекте — и прогоните go test -race ./.... Если найдёте баг, который модель помогла поймать, — считайте, что подборка уже окупилась.
Comments