Go без C-привычек: 12 промтов, которые заставят LLM писать идиоматичный код и находить data race

Каждый Go-разработчик рано или поздно сталкивается с ситуацией, когда LLM генерирует код, который компилируется, но выглядит как переписанный C: ручные мьютексы вместо каналов, if err != nil с паникой, отсутствие контекста и табличных тестов. Проблема не в модели, а в формулировке запроса. Если не задать явные ограничения, LLM будет тянуть паттерны из других языков.

Эта подборка — не просто список промтов. Это разбор того, как формулировать задачи, чтобы получать идиоматичный Go-код, ловить гонки данных в горутинах и каналах, писать табличные тесты и обрабатывать ошибки с учётом контекста. Каждый промт сопровождается примером результата и пояснением, для какой задачи он подходит.

Базовые промты: фундамент идиоматичного кода

1. Генерация кода с явными ограничениями

Задача: Получить функцию, которая не использует C-подобные паттерны (ручные циклы for с индексами, проверки на nil без необходимости, избыточные мьютексы).

Промт:

Напиши на Go функцию для [описание задачи]. Требования:
- Используй идиоматичные конструкции: range по срезу/каналу, defer для закрытия ресурсов, ошибки как значения.
- Не используй panic для обработки ошибок.
- Избегай ручных мьютексов, если можно использовать каналы или sync.WaitGroup.
- Добавь комментарии, объясняющие неочевидные решения.

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

func ProcessItems(ctx context.Context, items []string) error {
    if len(items) == 0 {
        return nil
    }
    results := make(chan string, len(items))
    var wg sync.WaitGroup
    for _, item := range items {
        wg.Add(1)
        go func(s string) {
            defer wg.Done()
            select {
            case results <- process(s):
            case <-ctx.Done():
            }
        }(item)
    }
    go func() {
        wg.Wait()
        close(results)
    }()
    for range results {
        // сбор результатов
    }
    return nil
}

Пояснение: Промт заставляет модель использовать context для отмены и каналы для синхронизации. Без явного указания LLM часто предлагает sync.Mutex и panic при ошибках.

2. Переписывание интерфейсов под Go-стиль

Задача: Преобразовать интерфейс из другого языка (например, Java) в идиоматичный Go-интерфейс.

Промт:

Дан интерфейс на Java:
interface DataProcessor {
    void process(List<String> data) throws IOException;
    String getResult();
}

Перепиши его на Go. Учти:
- В Go ошибки возвращаются как значения, а не бросаются.
- Избегай интерфейсов с методами, которые не используются вместе.
- Используй контекст для отмены операции.

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

type DataProcessor interface {
    Process(ctx context.Context, data []string) error
    Result() (string, error)
}

Пояснение: Промт заставляет модель разбить интерфейс на маленькие части и добавить context. В Go принято возвращать ошибку последним значением, а не использовать исключения.

Ключевое различие Идиоматичный Go избегает геттеров и сеттеров, если они не несут логики. Промт должен явно запрещать шаблонные методы вроде GetResult(), если они не нужны.

3. Обработка ошибок с контекстом

Задача: Сгенерировать функцию, которая оборачивает ошибки с дополнительным контекстом, но не теряет исходную ошибку.

Промт:

Напиши функцию на Go, которая читает файл и парсит JSON. При ошибке добавляй контекст (имя файла, номер строки) с помощью fmt.Errorf с %w. Не используй panic. Верни ошибку, которую можно проверить через errors.Is.

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

func LoadConfig(path string) (*Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("load config %s: %w", path, err)
    }
    var cfg Config
    if err := json.Unmarshal(data, &cfg); err != nil {
        return nil, fmt.Errorf("parse config %s: %w", path, err)
    }
    return &cfg, nil
}

Пояснение: Промт требует использовать %w для сохранения цепочки ошибок. Без этого LLM часто возвращает fmt.Errorf("failed to load config"), теряя исходную причину.

Продвинутые промты: горутины, каналы и тесты

4. Поиск data race в коде с горутинами

Задача: Найти потенциальные гонки данных в предложенном фрагменте.

Промт:

Проанализируй следующий код на Go на предмет data race. Укажи:
- Какие переменные разделяются между горутинами без синхронизации.
- Есть ли небезопасные операции чтения/записи.
- Предложи исправление с использованием каналов или sync/atomic.

Код:
func main() {
    var counter int
    for i := 0; i < 10; i++ {
        go func() {
            counter++
        }()
    }
    time.Sleep(time.Second)
    fmt.Println(counter)
}

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

Обнаружена гонка данных: переменная counter инкрементируется из 10 горутин без синхронизации. Исправление: используйте sync/atomic:
var counter int64
atomic.AddInt64(&counter, 1)
Или канал для сбора инкрементов.

Пояснение: Промт заставляет модель явно указать проблему и предложить два варианта решения. Важно, чтобы LLM не просто сказал «используйте мьютекс», а объяснил, почему выбранный способ лучше.

5. Генерация табличных тестов

Задача: Создать табличные тесты для функции с несколькими сценариями.

Промт:

Напиши табличные тесты для функции Divide(a, b float64) (float64, error). Покрой случаи: деление на ноль, отрицательные числа, нормальное деление. Используй t.Run для подтестов и проверяй ошибки через errors.Is.

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

func TestDivide(t *testing.T) {
    tests := []struct {
        name    string
        a, b    float64
        want    float64
        wantErr bool
    }{
        {"positive", 10, 2, 5, false},
        {"negative", -10, 2, -5, false},
        {"zero division", 1, 0, 0, true},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := Divide(tt.a, tt.b)
            if (err != nil) != tt.wantErr {
                t.Fatalf("Divide() error = %v, wantErr %v", err, tt.wantErr)
            }
            if got != tt.want {
                t.Errorf("Divide() = %v, want %v", got, tt.want)
            }
        })
    }
}

Пояснение: Промт требует использовать t.Run и структуру с полями. LLM без указания часто пишет отдельные тестовые функции, что затрудняет поддержку.

Что проверить первым Убедитесь, что в табличных тестах есть поле wantErr для проверки ошибок. Это помогает избежать ложных срабатываний, когда функция возвращает ошибку, но тест её не ожидает.

6. Рефакторинг с мьютекса на каналы

Задача: Переписать код с sync.Mutex на каналы для управления доступом к общему ресурсу.

Промт:

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

Исходный код:
type Counter struct {
    mu sync.Mutex
    val int
}
func (c *Counter) Inc() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.val++
}

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

type Counter struct {
    ch chan struct{}
    val int
}
func NewCounter() *Counter {
    c := &Counter{ch: make(chan struct{}, 1)}
    c.ch <- struct{}{}
    return c
}
func (c *Counter) Inc() {
    <-c.ch
    c.val++
    c.ch <- struct{}{}
}

Пояснение: Промт заставляет модель использовать канал как семафор. Однако важно понимать trade-off: каналы проще для понимания, но мьютексы эффективнее при высокой конкуренции. Промт должен требовать объяснения выбора.

7. Контекстная отмена в цепочке вызовов

Задача: Сгенерировать функцию, которая корректно обрабатывает отмену контекста и пробрасывает её в дочерние горутины.

Промт:

Напиши функцию FetchData(ctx context.Context) ([]byte, error), которая делает HTTP-запрос с таймаутом из контекста. При отмене контекста верни ошибку ctx.Err(). Используй http.NewRequestWithContext.

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

func FetchData(ctx context.Context) ([]byte, error) {
    req, err := http.NewRequestWithContext(ctx, "GET", "https://api.example.com/data", nil)
    if err != nil {
        return nil, err
    }
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    return io.ReadAll(resp.Body)
}

Пояснение: Промт требует использовать http.NewRequestWithContext. Без этого LLM может сгенерировать код с http.Get, который игнорирует отмену.

Экспертные промты: code review и сложные случаи

8. Code review с фокусом на идиоматичность

Задача: Проверить код на соответствие Go-идиомам и предложить улучшения.

Промт:

Проведи code review следующего кода на Go. Оцени:
- Использование ошибок (не игнорируются ли, обёрнуты ли с контекстом).
- Работу с горутинами (нет ли утечек, правильно ли закрываются каналы).
- Соответствие идиомам Go (избегание избыточных проверок, использование range вместо for i).

[код]

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

1. Ошибка в строке 10: err игнорируется. Добавьте проверку.
2. Горутина в строке 15 не завершается при отмене контекста — добавьте select с ctx.Done().
3. Вместо for i := 0; i < len(s); i++ используйте for _, v := range s.

Пояснение: Промт задаёт конкретные критерии, чтобы LLM не выдавал общие фразы. Экспертный уровень требует анализа утечек горутин и корректности закрытия каналов.

9. Борьба с утечками горутин

Задача: Найти и исправить утечку горутин в коде с каналами.

Промт:

Дан код, в котором горутина блокируется на отправке в канал, потому что получатель не читает. Найди утечку и предложи исправление с использованием select и ctx.Done().

Код:
func worker(ch chan int) {
    ch <- 42
}
func main() {
    ch := make(chan int)
    go worker(ch)
    // никто не читает ch
}

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

func worker(ctx context.Context, ch chan int) {
    select {
    case ch <- 42:
    case <-ctx.Done():
    }
}

Пояснение: Промт учит модель добавлять контекст для предотвращения вечной блокировки. Это классическая ошибка, которую LLM часто пропускает без явного указания.

Где чаще ошибаются При работе с каналами LLM часто забывает закрывать канал после отправки. Промт должен требовать явного закрытия в горутине-отправителе.

10. Генерация моков для интерфейсов

Задача: Создать мок-реализацию интерфейса для тестов без использования внешних библиотек.

Промт:

Сгенерируй мок для интерфейса UserRepository с методами GetUser(id int) (*User, error) и SaveUser(u *User) error. Мок должен возвращать заданные значения и ошибки. Используй только стандартную библиотеку.

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

type MockUserRepository struct {
    GetUserFunc  func(id int) (*User, error)
    SaveUserFunc func(u *User) error
}
func (m *MockUserRepository) GetUser(id int) (*User, error) {
    return m.GetUserFunc(id)
}
func (m *MockUserRepository) SaveUser(u *User) error {
    return m.SaveUserFunc(u)
}

Пояснение: Промт позволяет избежать зависимости от testify/mock. Такой мок легко настраивается в тестах через замыкания.

11. Оптимизация аллокаций в горячем пути

Задача: Переписать функцию, чтобы уменьшить количество аллокаций.

Промт:

Оптимизируй функцию на Go, чтобы уменьшить аллокации. Используй предварительное выделение слайса, избегай создания строк через конкатенацию в цикле, применяй strings.Builder.

Исходный код:
func joinWords(words []string) string {
    result := ""
    for _, w := range words {
        result += w + " "
    }
    return result
}

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

func joinWords(words []string) string {
    var sb strings.Builder
    sb.Grow(len(words) * 8) // примерная оценка
    for _, w := range words {
        sb.WriteString(w)
        sb.WriteByte(' ')
    }
    return sb.String()
}

Пояснение: Промт требует использовать strings.Builder и Grow. Это снижает количество аллокаций и копирований. Trade-off: код становится чуть сложнее, но выигрыш в производительности заметен на больших данных.

12. Генерация документации в формате godoc

Задача: Автоматически сгенерировать комментарии для экспортируемых функций и типов.

Промт:

Добавь godoc-комментарии к следующему коду на Go. Комментарии должны начинаться с имени функции/типа, описывать параметры, возвращаемые значения и возможные ошибки. Пример:
// Divide делит a на b. Возвращает ошибку, если b == 0.

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

// Divide делит a на b. Возвращает ошибку, если b == 0.
func Divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

Пояснение: Промт учит модель следовать стандарту godoc, что важно для публичных библиотек. Без указания LLM может писать комментарии в стиле «This function...», что не соответствует Go-стилю.

Что проверить первым Убедитесь, что комментарии начинаются с имени функции. Это требование gofmt и линтеров, например golint.

Выводы

Эти 12 промтов покрывают ключевые задачи Go-разработчика: от генерации идиоматичного кода до поиска гонок данных и написания табличных тестов. Главное правило — явно указывать ограничения: запрещать C-подобные паттерны, требовать использования контекста и каналов, проверять ошибки через errors.Is.

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

Для углубления в тему code review и работы с LLM загляните в статьи Промты для код-ревью: как заставить LLM находить баги до мержа и Tech Lead (менторство, code review).

← Все статьи

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

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

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

Цифровая гигиена за вечер: как найти утечки персональных данных и удалить информацию о себе из интернета

27 октября 2026

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

27 октября 2026

Cambridge Primary English (0058): почему начальные классы решают, будет ли ребёнок когда-либо читать бегло — и как на самом деле работает программа

27 октября 2026

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

28 октября 2026

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

28 октября 2026

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

29 октября 2026

Tech Lead (менторство, code review): как перейти от senior-разработчика к тимлиду в 2026 году и не сломать команду

29 октября 2026

Основы Computer Science: алгоритмы и структуры данных — кому подходит курс, а кому нет

11 ноября 2026

Запуск бизнеса с нуля: от идеи до первых продаж — комплексная программа для старта собственного дела

29 октября 2026