Каждый 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 принято возвращать ошибку последним значением, а не использовать исключения.
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 без указания часто пишет отдельные тестовые функции, что затрудняет поддержку.
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 часто пропускает без явного указания.
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-стилю.
Выводы
Эти 12 промтов покрывают ключевые задачи Go-разработчика: от генерации идиоматичного кода до поиска гонок данных и написания табличных тестов. Главное правило — явно указывать ограничения: запрещать C-подобные паттерны, требовать использования контекста и каналов, проверять ошибки через errors.Is.
Попробуйте применить эти промты в своём следующем проекте. Начните с базовых, постепенно переходя к экспертным. И помните: LLM — это инструмент, который усиливает ваши навыки, но не заменяет понимание языка. Чем точнее запрос, тем идиоматичнее результат.
Для углубления в тему code review и работы с LLM загляните в статьи Промты для код-ревью: как заставить LLM находить баги до мержа и Tech Lead (менторство, code review).