go generate на практике: рабочие приёмы кодогенерации в Go
Команда go generate — штатный механизм Go для автоматической генерации кода перед сборкой. Разбираем, как правильно оформлять директивы, какие генераторы использовать и почему наивный подход к кодогенерации превращает проект в хрупкую конструкцию.

Зачем вообще генерировать код в Go
Go — язык, который сознательно лишён макросов, шаблонов и декораторов. Когда нужно создать десятки однотипных методов, превратить SQL-схему в структуры или получить строковые представления констант, вариантов ровно два: писать руками или генерировать автоматически. Команда go generate, появившаяся ещё в Go 1.4, решает именно вторую задачу — и делает это проще, чем кажется на первый взгляд. Но «проще» не значит «без подводных камней».
Как работает go generate
Команда go generate не входит в цепочку go build. Она просто сканирует .go-файлы в пакете, находит комментарии вида //go:generate и выполняет указанную после них команду оболочки. Никакой магии — это буквально sh -c на содержимое строки.
Ключевой момент: go generate запускает разработчик (или CI), а не компилятор. Сгенерированные файлы коммитятся в репозиторий. Потребитель пакета не обязан ничего генерировать — он получает готовый код.
Что доступно внутри директивы
В строке //go:generate работают несколько встроенных переменных:
| Переменная | Значение |
|---|---|
$GOPACKAGE | Имя текущего пакета |
$GOFILE | Имя файла, в котором стоит директива |
$GOLINE | Номер строки директивы |
$GOARCH, $GOOS | Целевая архитектура и ОС |
$DOLLAR | Литерал $ (для экранирования) |
Этих переменных хватает, чтобы один и тот же генератор вызывать из разных файлов и пакетов без жёсткой привязки к путям.
Три типичных сценария
1. Строковые представления констант — stringer
Стандартная задача: есть набор iota-констант, нужен метод String(). Утилита stringer из пакета golang.org/x/tools делает это в одну директиву.
// status.go
package order
//go:generate go run golang.org/x/tools/cmd/stringer@latest -type=Status
type Status int
const (
StatusNew Status = iota // новый заказ
StatusPaid // оплачен
StatusShipped // отправлен
StatusDelivered // доставлен
StatusCancelled // отменён
)
После запуска go generate ./... в том же каталоге появится файл status_string.go с методом func (s Status) String() string. Метод использует таблицу индексов, а не switch — это быстрее и не забывает новые значения при добавлении константы.
Проверка: добавьте новую константу, забудьте перегенерировать — и String() вернёт Status(5) вместо имени. Как этого избежать, разберём ниже.
2. Моки для интерфейсов — mockgen
Если вы используете gomock (или его форк go.uber.org/mock), генерация моков выглядит так:
// repo.go
package storage
//go:generate go run go.uber.org/mock/mockgen@latest -source=repo.go -destination=mock_repo.go -package=storage
type UserRepo interface {
FindByID(id int64) (*User, error)
Save(user *User) error
}
Результат — файл mock_repo.go с полноценным моком, готовым к использованию в тестах. Обратите внимание на флаг -package: без него мок попадёт в отдельный пакет, и тесты придётся переписывать.
3. Собственные шаблоны через text/template
Когда готовый генератор не подходит, пишут свой — обычно небольшую программу в cmd/ каталоге проекта. Типичный паттерн:
// cmd/gen-validators/main.go
package main
import (
"os"
"text/template"
)
var tmpl = template.Must(template.New("validators").Parse(`// Code generated by gen-validators; DO NOT EDIT.
package {{.Package}}
{{range .Fields}}
func (s *{{$.Struct}}) Validate{{.Name}}() error {
if s.{{.Name}} == {{.Zero}} {
return fmt.Errorf("{{.Name}} is required")
}
return nil
}
{{end}}
`))
func main() {
// Здесь: парсинг аргументов, чтение AST или YAML-схемы,
// заполнение данных и вызов tmpl.Execute(os.Stdout, data)
_ = os.Stdout // заглушка для краткости примера
}
Директива в рабочем файле:
//go:generate go run ./cmd/gen-validators -struct=Order -package=order
Вся логика — обычный Go. Никаких DSL и плагинов к компилятору.
Сгенерированный файл обязан начинаться со строки, содержащей «Code generated … DO NOT EDIT.» — это стандартное соглашение Go. Линтеры и IDE распознают эту строку и не показывают предупреждения на сгенерированный код.
Типичные ошибки и чем за них платят
Забытая перегенерация
Самая частая проблема: код изменился, а go generate никто не запустил. Сгенерированные файлы в репозитории устарели, но компилируются — ошибка всплывает только в рантайме. Решение — проверка в CI:
# Шаг в CI-пайплайне (Go 1.21+)
go generate ./...
git diff --exit-code
Если git diff находит изменения, пайплайн падает с понятным сообщением: кто-то забыл закоммитить результат генерации.
Зависимость от системных утилит
Директива //go:generate protoc ... требует, чтобы protoc был установлен в системе определённой версии. Через полгода новый разработчик ставит другую версию — и сгенерированный код меняется. Два способа смягчить:
- Использовать
go run пакет@версиявместо вызова бинарника напрямую — версия зафиксирована. - Для утилит вне Go-экосистемы — фиксировать версию в
MakefileилиDockerfileсборочного контейнера и документировать это в README.
Генерация в init-время или при импорте
Иногда вместо go generate пытаются генерировать код через init() и os.WriteFile. Это антипаттерн: побочные эффекты при импорте, гонки при параллельных тестах, невозможность кросс-компиляции. Генерация — отдельный явный шаг, а не скрытая магия.
Слишком много логики в шаблоне
Шаблоны text/template тьюринг-полны. Соблазн велик — и через месяц в шаблоне появляются вложенные range, вызовы кастомных функций и проверки на nil в три уровня. Отлаживать такое невозможно: ошибка шаблона — это паника с невнятным смещением. Правило: если в шаблоне больше одного {{if}} внутри {{range}}, логику стоит вынести в Go-код, а в шаблон передавать уже подготовленные данные.
Сравнение подходов к генерации
| Подход | Когда подходит | Цена |
|---|---|---|
stringer, enumer и подобные | Строковые представления, JSON-маршалинг констант | Минимальная: одна директива, ноль своего кода |
mockgen, moq | Моки для тестов | Раздувает diff при каждом изменении интерфейса |
Собственный генератор на text/template | Нестандартный boilerplate, маппинг схем | Нужно писать и поддерживать генератор как отдельный проект |
Генерация через AST (go/ast, go/parser) | Генератор, который читает Go-код и дополняет его | Высокий порог входа, хрупкость при смене синтаксиса |
| Внешние кодогенераторы (protoc, sqlc, ent) | gRPC, SQL-first ORM, графы сущностей | Внешняя зависимость, фиксация версии, иногда свой плагин |
Критерий выбора прост: если готовый генератор закрывает задачу — берите его. Писать свой оправдано, когда шаблон специфичен для вашей доменной модели и его не переиспользует никто за пределами проекта.
Когда go generate — не лучший выбор
С появлением дженериков в Go 1.18 часть задач, которые раньше решались генерацией, решается параметризованными типами. Например, типобезопасные контейнеры и функции-хелперы для слайсов больше не требуют отдельного файла на каждый тип. Прежде чем добавлять директиву //go:generate, проверьте: может, хватит дженерика.
Второй случай — генерация, результат которой никогда не коммитится и запускается при каждой сборке. Это больше похоже на build step в Make или Task, и go generate тут лишняя абстракция: проще явный таргет в Makefile.
Что делать дальше
- Добавьте проверку
go generate ./... && git diff --exit-codeв CI — это окупается в первый же день. - Фиксируйте версии генераторов через
go run пакет@версияили черезtools.goс blank import иgo.sum. - Для сложных генераторов загляните в пакеты
go/astиgo/formatиз стандартной библиотеки: они позволяют генерировать и форматировать Go-код программно, без ручного склеивания строк. - Документация по
go generate—go help generateв терминале. Актуальная спецификация всегда в самом тулчейне.
Сноска: AST (Abstract Syntax Tree) — абстрактное синтаксическое дерево, древовидное представление исходного кода, где каждый узел соответствует конструкции языка: объявлению, выражению, оператору. В Go пакет go/ast позволяет программно читать и анализировать Go-файлы, что используется в генераторах и линтерах.
Генератор планировок: Набросок комнаты карандашом → 3D-рендер дизайна.
Спросить за 20 ₽Источники: Официальная документация Go: команда generate, Go Blog: Generating code
Частые вопросы
go generate запускается автоматически при go build?
Нужно ли коммитить сгенерированные файлы?
Что будет, если я добавлю константу, но забуду запустить go generate?
Как зафиксировать версию генератора, чтобы у всех в команде был одинаковый результат?
Дженерики в Go 1.18 заменяют кодогенерацию?
А если go generate выполняет команду, которой нет на машине коллеги?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.