Каждый, кто пробовал генерировать код на 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
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 приблизиться к этой культуре. Начните с базовых шаблонов, постепенно усложняйте требования и всегда проверяйте результат тестами. Так вы получите не просто работающий код, а код, который не стыдно показать на ревью."