Промты для Go-микросервисов: как LLM проектирует context, таймауты и graceful shutdown

Каждый, кто пробовал генерировать код на Go с помощью LLM, знает это чувство: просишь простой HTTP-сервер, а получаешь нечто среднее между Java и Python — с try/catch, глобальными переменными и паниками вместо ошибок. Модель обучена на огромном объёме кода, но без явных требований она скатывается к универсальным паттернам, которые в Go считаются плохим тоном. В этой статье я покажу, как формулировать промты, чтобы LLM выдавала идиоматичный Go: с явной обработкой ошибок, каналами вместо мьютексов и контекстами для управления временем жизни горутин.

Мы разберём три уровня промтов — базовые, продвинутые и экспертные — и для каждого приведём задачу, сам промт и пример результата. Вы увидите, как меняется качество кода в зависимости от детализации требований, и научитесь выбирать подходящий уровень для своих задач. Если ваша цель — быстро получить рабочий код без гонок данных и с понятной архитектурой, эти шаблоны сэкономят вам часы отладки.

Базовые промты: когда задача простая, но стиль важен

Базовый промт — это не просто «напиши код на Go». Это минимальный набор требований, который отсекает неидиоматичные решения. Например, если вы просите CRUD-обработчик, укажите, что ошибки должны возвращаться явно, а не через панику, и что нужно использовать стандартную библиотеку без лишних зависимостей.

Промт 1: CRUD-обработчик с явной обработкой ошибок

Задача: написать HTTP-обработчик для создания пользователя, который принимает JSON, валидирует поля и возвращает ошибки в формате JSON.

Промт:

Напиши на Go HTTP-обработчик для создания пользователя (POST /users). Требования:
- Используй только стандартную библиотеку (net/http, encoding/json).
- Обрабатывай ошибки явно: возвращай JSON с полем "error" и соответствующим HTTP-статусом.
- Не используй панику для контроля потока.
- Валидируй, что поля name и email не пустые, email содержит @.
- При успехе возвращай 201 и созданный объект с ID (сгенерируй UUID-like строку).
- Код должен быть готов к вставке в существующий проект, без лишних комментариев.

Пример результата (сокращённо):

func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
    var user struct {
        Name  string `json:"name"`
        Email string `json:"email"`
    }
    if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
        respondError(w, http.StatusBadRequest, "invalid JSON")
        return
    }
    if user.Name == "" || user.Email == "" || !strings.Contains(user.Email, "@") {
        respondError(w, http.StatusBadRequest, "name and email are required, email must contain @")
        return
    }
    user.ID = generateID() // псевдо-UUID
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(user)
}

func respondError(w http.ResponseWriter, status int, msg string) {
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(map[string]string{"error": msg})
}

Без указания «не используй панику» модель вполне может сгенерировать panic("invalid input"). Базовый промт задаёт рамки, но не углубляется в конкурентность.

Ключевое различие Базовый промт подходит для линейных задач без параллелизма. Если в задаче есть горутины или сетевые вызовы, переходите к продвинутым промтам — иначе риск гонок данных и утечек горутин возрастает на порядок.

Продвинутые промты: конкурентность и контексты

Когда в игру вступают горутины и каналы, промт должен явно требовать идиоматичных паттернов: context.Context для отмены, sync.WaitGroup для ожидания, каналы для коммуникации вместо общих переменных. Модель часто предлагает мьютексы там, где достаточно канала, или забывает про defer cancel().

Промт 2: параллельный обход URL с ограничением конкурентности

Задача: написать функцию, которая принимает список URL, параллельно делает GET-запросы с лимитом одновременных запросов (например, 5) и возвращает результаты или ошибки.

Промт:

Напиши на Go функцию FetchAll(ctx context.Context, urls []string, limit int) ([]Result, error), которая:
- Параллельно выполняет HTTP GET для каждого URL, но не более limit одновременно.
- Использует горутины и каналы для ограничения конкурентности (семафор через буферизованный канал).
- Принимает context.Context и корректно обрабатывает отмену: если ctx.Done(), прекращает новые запросы и возвращает ошибку.
- Возвращает срез результатов с полями URL, StatusCode, Error (если запрос не удался).
- Не использует мьютексы для синхронизации — только каналы.
- Обеспечь отсутствие утечек горутин: все горутины должны завершаться.

Пример результата (ключевая часть):

type Result struct {
    URL        string
    StatusCode int
    Error      error
}

func FetchAll(ctx context.Context, urls []string, limit int) ([]Result, error) {
    sem := make(chan struct{}, limit)
    results := make([]Result, len(urls))
    var wg sync.WaitGroup

    for i, url := range urls {
        select {
        case <-ctx.Done():
            return nil, ctx.Err()
        case sem <- struct{}{}:
        }
        wg.Add(1)
        go func(i int, url string) {
            defer wg.Done()
            defer func() { <-sem }()
            req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
            if err != nil {
                results[i] = Result{URL: url, Error: err}
                return
            }
            resp, err := http.DefaultClient.Do(req)
            if err != nil {
                results[i] = Result{URL: url, Error: err}
                return
            }
            defer resp.Body.Close()
            results[i] = Result{URL: url, StatusCode: resp.StatusCode}
        }(i, url)
    }
    wg.Wait()
    return results, nil
}

Здесь важно, что промт запрещает мьютексы и требует context. Без этого модель может сгенерировать sync.Mutex вокруг общего среза, что менее идиоматично и чревато блокировками.

Что проверить первым Убедитесь, что в сгенерированном коде есть defer cancel() для контекста с таймаутом и что все горутины завершаются при отмене. Частая ошибка LLM — забыть про wg.Wait() или использовать time.Sleep вместо синхронизации.

Промт 3: worker pool с динамическим добавлением задач

Задача: реализовать пул воркеров, который обрабатывает задачи из канала и поддерживает корректное завершение по сигналу.

Промт:

Реализуй на Go паттерн worker pool. Функция StartWorkers(ctx context.Context, numWorkers int, jobs <-chan Job) []<-chan Result:
- Каждый воркер читает из канала jobs и пишет в свой выходной канал.
- При закрытии канала jobs или отмене ctx воркеры должны завершиться, закрыв свои выходные каналы.
- Используй sync.WaitGroup для ожидания завершения всех воркеров и горутину для закрытия каналов результатов.
- Не используй глобальные переменные и панику.
- Покажи пример использования с таймаутом.

Пример результата:

type Job struct{ ID int }
type Result struct{ JobID int; Data string }

func StartWorkers(ctx context.Context, numWorkers int, jobs <-chan Job) []<-chan Result {
    results := make([]<-chan Result, numWorkers)
    var wg sync.WaitGroup
    for i := 0; i < numWorkers; i++ {
        out := make(chan Result)
        results[i] = out
        wg.Add(1)
        go func() {
            defer wg.Done()
            defer close(out)
            for {
                select {
                case <-ctx.Done():
                    return
                case job, ok := <-jobs:
                    if !ok {
                        return
                    }
                    // обработка
                    select {
                    case out <- Result{JobID: job.ID, Data: "processed"}:
                    case <-ctx.Done():
                        return
                    }
                }
            }
        }()
    }
    go func() {
        wg.Wait()
    }()
    return results
}

Продвинутые промты заставляют модель явно прописывать механизмы отмены и синхронизации, что снижает вероятность гонок данных.

Экспертные промты: архитектура и тестирование на гонки

Экспертный уровень — это когда вы просите LLM не просто написать код, а спроектировать решение с учётом граничных случаев, написать тесты и даже проверить на гонки. Здесь важно давать модели роль и контекст.

Промт 4: ревью кода на предмет гонок данных

Задача: проверить существующий код на наличие гонок и предложить исправления.

Промт:

Ты — опытный Go-разработчик, специализирующийся на конкурентности. Проанализируй следующий код на предмет гонок данных и утечек горутин. Предложи исправления, используя идиоматичные паттерны Go (каналы, context, sync.WaitGroup). Объясни, почему возникла проблема.

Код:
[вставьте свой код]

Пример результата: модель укажет на отсутствие синхронизации при доступе к общей мапе и предложит заменить её на канал или sync.Map, а также добавит context для отмены.

Промт 5: генерация тестов с проверкой на гонки

Задача: написать unit-тесты для функции с горутинами, включая запуск с флагом -race.

Промт:

Напиши тесты на Go для функции FetchAll (из предыдущего примера). Требования:
- Используй стандартный пакет testing.
- Проверь успешные запросы, ошибки HTTP и отмену контекста.
- Добавь тест, который запускается с флагом -race (в комментарии укажи команду go test -race).
- Используй httptest для имитации сервера.
- Убедись, что тесты не флакают.

Пример результата:

func TestFetchAll(t *testing.T) {
    ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
    }))
    defer ts.Close()
    urls := []string{ts.URL, ts.URL}
    results, err := FetchAll(context.Background(), urls, 2)
    if err != nil {
        t.Fatal(err)
    }
    for _, res := range results {
        if res.StatusCode != http.StatusOK {
            t.Errorf("expected 200, got %d", res.StatusCode)
        }
    }
}
// Запуск: go test -race
Где чаще ошибаются LLM часто забывает добавить t.Parallel() или неправильно обрабатывает отмену контекста в тестах. Всегда проверяйте, что тесты действительно покрывают сценарии с отменой.

Промт 6: проектирование API с учётом конкурентности

Задача: спроектировать in-memory хранилище с конкурентным доступом.

Промт:

Спроектируй на Go потокобезопасное in-memory key-value хранилище. Требования:
- Поддержка Get, Set, Delete.
- Использование RWMutex для чтения и записи, но с возможностью замены на каналы, если это упростит код.
- Добавь context.Context в методы для управления таймаутами.
- Напиши пример использования с горутинами, которые одновременно читают и пишут.
- Объясни, почему выбран тот или иной примитив синхронизации.

Пример результата: модель предложит структуру с sync.RWMutex и методами, принимающими контекст, а также продемонстрирует, как избежать дедлоков.

Промт 7: рефакторинг с мьютексов на каналы

Задача: переписать код, использующий мьютексы, на идиоматичные каналы.

Промт:

Перепиши следующий код на Go, заменив sync.Mutex на каналы для синхронизации. Сохрани функциональность. Объясни, какие преимущества и недостатки у такого подхода.

Код:
[вставьте код с мьютексом]

Пример результата: LLM может предложить паттерн «владелец горутины» (goroutine ownership), где состояние защищено каналом, но предупредит о возможном снижении производительности.

Промт 8: обработка ошибок в горутинах

Задача: собрать ошибки из нескольких горутин и вернуть первую или агрегированную.

Промт:

Напиши функцию, которая запускает несколько горутин, каждая может вернуть ошибку. Функция должна дождаться всех и вернуть агрегированную ошибку (можно использовать errors.Join из Go 1.20+). Используй errgroup или каналы. Не используй панику.

Пример результата: модель предложит использовать golang.org/x/sync/errgroup или собственную реализацию с sync.WaitGroup и каналом ошибок.

Что проверить первым Убедитесь, что при использовании errgroup вы не забыли про WithContext, чтобы отменять остальные горутины при первой ошибке.

Как выбрать уровень промта

Выбор зависит от сложности задачи. Для простого CRUD достаточно базового промта с указанием стиля. Если в задаче есть параллелизм, сетевые вызовы или работа с общими данными — переходите на продвинутый уровень с явными требованиями к context и каналам. Экспертные промты нужны, когда важна архитектура, тестирование или ревью.

Уровень Когда использовать Ключевые требования
Базовый CRUD, обработчики, утилиты без конкурентности Явная обработка ошибок, stdlib, без паники
Продвинутый Горутины, каналы, context, пулы воркеров context, sync.WaitGroup, каналы вместо мьютексов, отсутствие утечек
Экспертный Архитектура, тесты, ревью, рефакторинг Роль эксперта, тесты с -race, анализ trade-off

Помните: даже с идеальным промтом результат нужно проверять. LLM может ошибаться в деталях, особенно в обработке граничных случаев. Всегда запускайте go test -race и просматривайте сгенерированный код.

Если вы хотите глубже разобраться в конкурентности Go и смежных темах, обратите внимание на наш разбор Go и Rust — системное программирование: разбор реальной задачи и подводные камни и материал об LLM-инженерии с LangChain.

Идиоматичный Go — это не только синтаксис, но и культура обработки ошибок и конкурентности. Чёткие промты помогают LLM приблизиться к этой культуре. Начните с базовых шаблонов, постепенно усложняйте требования и всегда проверяйте результат тестами. Так вы получите не просто работающий код, а код, который не стыдно показать на ревью."

← Все статьи

Спросить AI по этой статье

Задайте вопрос по этой статье — ASI Biont ответит, опираясь на её текст.

Читайте также

ИИ для повседневной жизни: как построить реальный семейный бюджет с ChatGPT и Claude (без кода, 2026)

27 октября 2026

Компьютерные сети (CCNA): собираем WireGuard-сервер на VPS — практический гайд по VPN и сетевой безопасности

27 октября 2026

Сколько часов учить испанский до DELE 2026: разбор нормативов, статистики и гибридной подготовки с AI-репетитором

27 октября 2026

Юнит-тесты на Python с LLM: 12 промтов от pytest-фикстур до Hypothesis, которые закрывают непокрытые ветки

27 октября 2026

Full-Stack AI Engineer: как RAG-пайплайны, векторные базы данных и развертывание LLM на самом деле работают в продакшене

28 октября 2026

CIPM / CIPP/E — Управление конфиденциальностью и GDPR (IAPP): как статистика правоприменения GDPR превращает риск несоответствия требованиям в статью бюджета

28 октября 2026

Сверка актов вручную или промптом: где LLM экономит бухгалтеру часы, а где создаёт риски

12 ноября 2026

Полный курс программирования на Python: «Python с нуля» — от переменных до веб-приложений

28 октября 2026

Quant Finance и Structured Products — количественные финансы: как на самом деле считают рыночный риск и где ломаются модели

29 октября 2026