JavaRush /Курси /Go SELF /go generate: користь ...

go generate: користь і прозорість

Go SELF
Рівень 36 , Лекція 4
Відкрита

1. Вступ

Якщо ви колись писали одне й те саме двічі, ви вже стоїте на порозі генерації коду. Зазвичай усе починається безневинно: «ну, зараз я швиденько зроблю String() на switch». Потім з’являється другий enum, потім третій. А далі ви перейменували значення, забули оновити рядок — і ось перед вами баг, який дуже важко знайти, бо він виглядає як «просто неправильний текст». У цей момент ви починаєте розуміти: ручний шаблонний код — це не героїзм, а технічний борг, замаскований під продуктивність.

go generate — це механізм, який дає змогу явно запускати генератори коду, пов’язані з вихідними файлами, через спеціальні коментарі. Тобто генерація перестає бути таємним ритуалом «у когось на ноутбуці все працює» і стає відтворюваним кроком, який ви бачите прямо в репозиторії.

І одразу важлива думка дня: генерація — це не «магія», а просто автоматизація нудних повторів. Якщо вона починає змінювати зміст програми залежно від фази місяця чи часу доби, це вже не генерація, а сюжет для трилера.

2. Як працює go generate

Важливо розуміти механіку без містики: go generate сканує вихідні файли Go і шукає рядки-коментарі спеціального формату. Для кожної знайденої директиви він запускає вказану команду. При цьому go generateне частина go build: він нічого не «наздожене» сам і не аналізує залежності. Його потрібно запускати явно, коли ви вирішили оновити згенеровані файли.

Це розділяє процес на дві зрозумілі фази: «я змінив вхідні дані» і «я запустив генерацію та отримав оновлений результат». У Go такий підхід обрали спеціально: щоб збірка залишалася передбачуваною й швидкою, а генерація — усвідомленою.

Щоб картина була зовсім ясною, уявіть типовий цикл роботи у вигляді схеми:

flowchart TD
    A[Правимо вихідні файли: enum, теги, схеми] --> B[Запускаємо go generate]
    B --> C[Оновилися *_gen.go / *_string.go]
    C --> D[go test]
    D --> E[Коміт: вихідні файли + згенеровані файли]

І ще один принциповий момент: go generate задуманий насамперед для автора пакета, а не для користувача вашої бібліотеки. Тому згенеровані файли зазвичай комітять у репозиторій, щоб користувачеві не потрібно було мати в себе всі генератори.

Директива //go:generate

З директивами в Go підхід простий: щоб рядок вважався директивою, він має починатися рівно з //go:generate — без пробілу після //. Якщо написати // go:generate, ви просто залишите коментар «для майбутніх археологів», а команда не запуститься. Цей суворий формат — частина ідеї «жодної магії»: або директиву розпізнано однозначно, або ні.

Зазвичай директиву розміщують поруч із тим місцем, заради якого генерація потрібна. Не «в якомусь окремому файлі генерації на 300 рядків», а прямо біля типу, констант або структури, які є джерелом правди. Тоді читач бачить: ось тип, ось константи, ось команда, яка генерує підтримку.

Мініприклад «як виглядає директива» (поки без прив’язки до нашого проєкту):

package todo

//go:generate stringer -type=Status
type Status int

Команда, яку ви вкажете після //go:generate, запускається як зовнішня програма. Часто це один із трьох варіантів: окрема утиліта (наприклад, stringer), go run ./internal/... для запуску вашого генератора або щось на кшталт goyacc для генерації парсерів. Сам факт, що Go не обмежує вас однією-єдиною утилітою, і робить go generate універсальним.

Запускати генерацію можна точково — у поточному каталозі — або рекурсивно по проєкту. На практиці зазвичай використовують одну з команд: go generate (поточний каталог) або go generate ./... (усі пакети рекурсивно). У цій лекції ми тримаємо в голові другий варіант як «оновити все».

3. Приклад: Status і генерація String() через stringer

Зараз зберемо шматочок нашого навчального застосунку — пакет todo — так, щоб він був максимально життєвим: у задачі є статус, і нам хочеться друкувати його по-людськи. Можна, звісно, завжди друкувати числа… але тоді ваш консольний застосунок виглядатиме як бухгалтерія в мініатюрі.

Почнемо з простого Status на базі int і набору констант через iota:

package todo

// Status describes the lifecycle state of a task.
type Status int

const (
	StatusUnknown Status = iota
	StatusOpen
	StatusDone
)

Тепер наївний шлях: написати метод String() вручну. Це чесно, працює і на перших порах навіть корисно, щоб зрозуміти механіку:

package todo

import "fmt"

// String returns a human-readable status name.
func (s Status) String() string {
	switch s {
	case StatusOpen:
		return "open"
	case StatusDone:
		return "done"
	default:
		return fmt.Sprintf("Status(%d)", s)
	}
}

Проблема тут не в тому, що код поганий. Проблема в тому, що він швидко перестає бути «одноразовим»: додали StatusArchived, перейменували StatusOpen на StatusActive, змінили порядок — і ви зобов’язані синхронно оновити String(). Ось тут генерація — ідеальний інструмент: це механічна робота, яку комп’ютер виконає стабільніше за вас.

Підключаємо stringer через go generate

У Go є популярна утиліта stringer із репозиторію golang.org/x/tools, яка якраз генерує String() для наборів цілих констант. Важливо: stringer не є частиною стандартної бібліотеки, але він «рідний за духом» і широко використовується.

Додамо директиву поруч із типом:

package todo

//go:generate stringer -type=Status

type Status int

const (
	StatusUnknown Status = iota
	StatusOpen
	StatusDone
)

Тепер, коли ви виконаєте go generate у цьому пакеті, stringer створить файл, зазвичай із суфіксом _string.go, зі згенерованим методом String().

Перевіряємо ефект у маленькому коді

Щоб відчути користь, давайте зробимо мініфрагмент, який друкує статус. Це може бути шматочок main вашого CLI або просто тимчасовий експеримент:

package main

import (
	"fmt"

	"example.com/todoapp/todo"
)

func main() {
	st := todo.StatusDone
	fmt.Println(st) // StatusDone після генерації stringer
}

До генерації fmt.Println(st) друкував би число (2), якщо String() немає. Після генерації fmt побачить метод String() і виведе рядок. І так, це той випадок, коли гарний вивід — не косметика, а зручність налагодження та UX.

4. Як жити зі згенерованими файлами

Коли генератор створює файл, він зазвичай додає в шапку попередження на кшталт Code generated ... DO NOT EDIT.. Це не погроза, а турбота про вашу психіку: щоб ви не витратили годину на правку файла, який за хвилину буде перегенеровано й перезаписано. У прикладі зі stringer це вважається хорошим тоном: згенерований файл починається з такого рядка.

Виглядати це може приблизно так, якщо спростити реалізацію stringer:

// Code generated by stringer -type=Status; DO NOT EDIT.

package todo

func (s Status) String() string {
	// ... згенероване зіставлення ...
	return ""
}

Тепер про головний організаційний момент: що робити з цим файлом у git? У Go прийнято зберігати згенеровані файли поруч із вихідниками, якщо вони потрібні для збірки або використання пакета, тому що користувач вашого пакета не зобов’язаний уміти запускати ваші генератори. Це робить проєкт менш крихким: збірка залежить від вихідних файлів і вже згенерованого результату, а не від наявності інструментів на машині користувача.

Звідси випливає вимога детермінованості: якщо два розробники запустять go generate на одній і тій самій версії вихідників, результат має бути однаковим. Інакше git-диф перетворюється на шум, а команда — на клуб «у кого сьогодні вийшло».

Практично детермінованість зазвичай ламають три речі: генерація, що залежить від поточного часу; генерація, що залежить від випадковості; генерація, що залежить від оточення — шляхів, локалі, версій утиліт. Тому «здоровий» генератор намагається бути максимально чистим: однаковий вхід → однаковий вихід.

5. Як не зробити проєкт «магічним»

Найчастіша біда з go generate не технічна, а організаційна: ви додали генерацію, усе працювало, а за місяць ніхто вже не пам’ятає, що її треба запускати, якою версією інструмента і чому у вас взагалі з’явився файл something_gen.go, який не можна чіпати. Щоб цього не сталося, варто дотримуватися кількох простих правил. Вони звучать нудно, зате рятують проєкт від «паранормальних явищ».

Перше правило — генерація має бути явною і не прикидатися частиною збірки. Це філософія go generate як команди: її спеціально не вбудовано в go build. Тоді у вас завжди є зрозуміле питання: «ти запускав генерацію?» — і зрозуміла відповідь: «так / ні».

Друге правило — команда генерації має бути видимою в коді, поруч із джерелом даних. Саме тому директива розміщується поруч із Status, а не десь у каталозі scripts. Коли ви читаєте тип, ви одразу бачите: «ага, тут stringer, отже String() генерується».

Третє правило — генерація має обслуговувати те, що справді механічне. String() для enum — чудовий приклад: воно не додає бізнес-сенсу, а просто позбавляє вас рутини й синхронізації рядків із константами.

Четверте правило — документуйте запуск генерації там, де люди реально дивляться. Зазвичай це README і/або doc.go. Якщо в пакеті є генерація, у README проєкту цілком доречна коротка секція «Development», де сказано: «після зміни статусів виконайте go generate ./...».

П’яте правило — тримайте приклади й тести на боці правди. Example-тести хороші тим, що не дають документації протухнути: якщо ви показуєте в прикладі fmt.Println(StatusDone), то при поломці генерації приклад почне падати або принаймні перестане відповідати очікуваному виводу. Це саме та ідея «документації, яку можна виконати», заради якої й існують example-тести.

Ось маленький Example, який можна покласти в status_example_test.go і тим самим «прибити цвяхами» очікування від генерації:

package todo

import "fmt"

func ExampleStatus_String() {
	fmt.Println(StatusDone)
	// Output: StatusDone
}

Так, stringer за замовчуванням друкує ім’я константи (StatusDone), а не "done". І це нормально: генератор робить рівно те, що обіцяє. Якщо вам потрібно «done/open», це вже окрема політика відображення. Її можна зробити вручну, а stringer залишити для технічного налагодження. Важливо, що приклад фіксує контракт і стає перевірюваним.

6. Типові помилки під час роботи з go generate

Помилка №1: очікувати, що генерація відбудеться «сама під час go build».
Це, мабуть, наймасовіша пастка. Після кількох днів у Go мозок звикає: «я написав код — і воно зібралося». Але go generate спеціально не вбудовано в збірку й має запускатися окремо, без автоматичного аналізу залежностей. Якщо про це забути, ви отримаєте ситуацію «у мене все працює, а в колеги не компілюється», бо у вас локально лежить свіжий згенерований файл.

Помилка №2: написати // go:generate і дивуватися, чому нічого не відбувається.
Директива розпізнається лише у строгому форматі //go:generate. Жодних «ну ви ж зрозуміли, що я мав на увазі». Go тут як компілятор: він не читає думки, він читає символи. Це неприємно рівно один раз, а потім ви починаєте любити передбачуваність.

Помилка №3: правити згенерований файл руками.
Згенерований файл часто починається з «DO NOT EDIT» не зі шкідливості. Якщо ви підправили його вручну, а потім хтось запустив go generate, ваша правка зникне без сліду, ніби її ніколи не було. Правильна точка змін — або вхідні дані (enum/константи), або параметри генератора, або сам генератор.

Помилка №4: зробити генерацію недетермінованою.
Якщо генерація додає поточну дату, випадкові числа, порядок обходу map або залежність від локалі, ви отримаєте нескінченні дифи й конфліктні коміти. Сама ідея «нудний код генерує машина» передбачає повторюваність. Інакше ви просто змінюєте один вид болю на інший, лише більш екзотичний.

Помилка №5: сховати генерацію й відірвати її від сенсу.
Коли директива стоїть далеко від типу, заради якого вона потрібна, новий розробник сприйматиме генерацію як раптове природне явище: «у каталозі є якийсь файл _string.go, мабуть, його приніс вітер». Набагато краще тримати //go:generate поруч із джерелом даних — у нашому прикладі поруч із Status — щоб зв’язок читався очима.

1
Опитування
Документація, рівень 36, лекція 4
Недоступний
Документація
Документація
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ