[Перевод] Генерация кода в Go
Перевод статьи Роба Пайка из официального блога Go о автоматической кодогенерации с помощью go generate. Статья немного устарела (была написана перед выходом Go 1.4, в котором и появился go generate), но хорошо объясняет суть работы go generate.
Одно из свойств теории вычислимости — полнота по Тьюрингу — заключается в том, что программа может написать другую программу. Это мощная идея, которая не настолько оценена, как того заслуживает, хотя и встречается достаточно часто. Это достаточно весомая часть определения того, что делают компиляторы, например. Также, команда go test работает тоже по такому же принципу: она сканирует пакеты, которые нужно тестировать, создаёт новую Go программу, в которой дописан необходимый обвес для тестов, затем компилирует и запускает её. Современные компьютеры настолько быстры, что такая, казалось бы, дорогая последовательность действий исполняется за доли секунды.
Вокруг есть масса других примеров, когда программы пишут программы. Yacc, к примеру, читает описание грамматики и выдаёт программу, которая парсит эту грамматику. «Компилятор» Protocol Buffers читает описание интерфейса и выдает определения структур, методов и прочего кода. Разнообразные утилиты конфигурации работают похожим образом тоже, извлекая метаданные из среды окружения и создавая кастомные команды запуска.
Таким образом, программы, пишущие программы являются важным элементом в разработке ПО, но программы вроде Yacc, которые создают исходный код, должны быть интегрированы в процесс сборки, чтобы их вывод мог быть передан компилятору. Когда используется внешняя система сборки, вроде Make, это обычно просто сделать. Но в Go, в котором утилита go получает всю необходимую информацию о билде из исходных кодов, это проблема. В нём просто нет механизма, чтобы запустить Yacc с помощью go tool.
До этого момента, в смысле.
Последний релиз Go, 1.4, включает в себя новую команду, go generate, которая позволяет запускать подобные утилиты. Она называется go generate, и при запуске сканирует код на наличие специальных комментариев, которые указывают, какие команды нужно запускать. Важно понимать, что go generate не является частью go build. Она не анализирует зависимости и должна быть запущена до go build. Она предназначена для автора Go пакета, а не для его пользователей.
Команда go generate очень проста в использовании. Для разминки, вот как её использовать, чтобы сгенерировать Yacc грамматику. Скажем, у вас есть входной Yacc-файл, называющийся gopher.y, который определяет грамматику вашего нового языка. Чтобы сгенерировать код на Go, который будет парсить эту грамматику, вы обычно запустили бы стандартную Go-версию yacc, как-нибудь так:
go tool yacc -o gopher.go -p parser gopher.y
Опция -o тут указывает имя результирующего файла, а -p — имя пакета.
Чтобы переложить этот процесс на go generate, нужно в любом обычном (не автосгенерированном) .go файле в этой директории добавить вот такой комментарий://go:generate go tool yacc -o gopher.go -p parser gopher.y
Этот текст это та же самая команда, но с добавленным вначале комментарием, который распознает go generate. Комментарий должен начинаться в начале строки и не иметь пробелов между // и go:generate. После этого маркера, оставшаяся часть указывает, какую команду go generate должен запускать.
А теперь запустите её. Перейдите в исходную директорию и запустите go generate, затем go build и так далее:
$ cd $GOPATH/myrepo/gopher
$ go generate
$ go build
$ go test
И это всё, что нужно. Если нет никаких ошибок, то go generate вызовет yacc, который создаст gopher.go, на этом моменте директория будет содержать все необходимые go-файлы, которые мы можем собирать, тестировать и нормально с ними работать. Каждый раз, когда gopher.y меняется, просто перезапустите go generate, чтобы пересоздать парсер.
Если вам интересно больше деталей о том, как go generate работает внутри, включая параметры, переменные окружения и так далее, смотрите документ с описанием дизайна.
Go generate не делает ничего, что не могло бы быть сделано с помощью Make или другого механизма сборки, но он идёт из коробки в команде go — не нужно ничего устанавливать дополнительно — и он хорошо вписывается в экосистему Go. Главное, помните, что это для авторов пакета, не для пользователей, хотя бы из соображений того, что программа, которая будет вызываться, может отсутствовать на машине пользователя. Также, если пакет предполагается использоваться с go get, не забывайте внести сгенерированные файлы в систему контроля версий, сделав доступными для пользователей.
Теперь, давайте посмотрим, как можно использовать это для чего-то нового. В качестве радикально иного примера, где go generate может помочь, в репозитории golang.org/x/tools есть новая программа stringer. Она автоматически генерирует строковые методы String() для наборов числовых констант. Она не входит в стандартный набор Go, но её легко установить:
$ go get golang.org/x/tools/cmd/stringer
Вот пример из документации к stringer. Представьте, что у нас есть некоторый код, с набором числовых констант, определяющих разные типы лекарств:
package painkiller
type Pill int
const (
Placebo Pill = iota
Aspirin
Ibuprofen
Paracetamol
Acetaminophen = Paracetamol
)
Для отладочных целей, мы хотели бы, чтобы эти константы могли красиво отдавать своё название, другими словами, мы хотим метод со следующей сигнатурой:
func (p Pill) String() string
Его легко написать вручную, например как-то так:
func (p Pill) String() string {
switch p {
case Placebo:
return "Placebo"
case Aspirin:
return "Aspirin"
case Ibuprofen:
return "Ibuprofen"
case Paracetamol: // == Acetaminophen
return "Paracetamol"
}
return fmt.Sprintf("Pill(%d)", p)
}
Есть несколько способов написать эту функцию, разумеется. Мы можем использовать слайс строк, индексированный по Pill, или map, или какую-нибудь другую технику. Так или иначе, мы должны поддерживать его каждый раз, когда мы меняем набор лекарств, и мы должны проверять, что код правильный. (Два разных названия для парацетамола, к примеру, делают этот код чуть более мудрёным, чем он мог бы быть). Плюс, сам вопрос выбора способа реализации зависит от типов значений: знаковое или беззнаковое, плотное и разбросанное, начинающиеся с нуля или нет и так далее.
Программа stringer берёт эти заботы на себя. Хотя она может запускаться и вручную, но она предназначена для запуска через go generate. Чтобы использовать её, добавьте комментарий в исходник, скорее всего, в коде с определением типа://go:generate stringer -type=Pill
Это правило указывает, что go generate должна запустить команду stringer, чтобы сгенерировать метод String для типа Pill. Вывод автоматически будет записан в файл pill_string.go (вывод может быть переопределён с помощью флага -output).
Давайте запустим её:
$ go generate
$ cat pill_string.go
// generated by stringer -type Pill pill.go; DO NOT EDIT
package pill
import "fmt"
const _Pill_name = "PlaceboAspirinIbuprofenParacetamol"
var _Pill_index = [...]uint8{0, 7, 14, 23, 34}
func (i Pill) String() string {
if i < 0 || i+1 >= Pill(len(_Pill_index)) {
return fmt.Sprintf("Pill(%d)", i)
}
return _Pill_name[_Pill_index[i]:_Pill_index[i+1]]
}
$
Каждый раз, когда мы меняем определение Pill или констант, всё что мы должны сделать, это запустить
$ go generate
чтобы обновить String метод. И конечно, если у нас есть несколько типов в одном пакете, которые нужно обновить, go generate обновит их всех.
Само собой разумеется, что сгенерированный код уродлив. Это OK, впрочем, так как люди не будут работать с этим кодом; автосгенерированный код очень часто уродлив. Он старается быть максимально эффективным. Все имена объединены вместе в одну строку, которая экономит память (всего одна строка на все имена, даже их тут несметное количество). Затем массив, _Pill_index, находит соответствие типа с именем используя простую и очень эффективную технику. Обратите внимание, что _Pill_index это массив (не слайс; одним заголовком меньше) значений типа uint8, наимейший возможный целочисленный тип, способный вместить в себя нужные значения. Если значений будет больше, или будут отрицательные, тип сгенерированного массива _Pill_index может поменяться на uint16 или int8, смотря что будет работать лучше.
Подход, используемый в методах, сгенерированных с помощью stringer меняется, в зависимости от свойств набора констант. К примеру, если константы разряженные, он может использовать map. Вот простой пример, основанный на наборе констант, представляющих степени двойки:
const _Power_name = "p0p1p2p3p4p5..."
var _Power_map = map[Power]string{
1: _Power_name[0:2],
2: _Power_name[2:4],
4: _Power_name[4:6],
8: _Power_name[6:8],
16: _Power_name[8:10],
32: _Power_name[10:12],
...,
}
func (i Power) String() string {
if str, ok := _Power_map[i]; ok {
return str
}
return fmt.Sprintf("Power(%d)", i)
}
Резюмируя, автоматическая генерация метода позволяет нам решать задачу лучше, чем это сделал бы человек.
В исходных кодах Go есть масса других примеров использования go generate. Сюда входят генерация таблиц Unicode в пакете unicode, создание эффективных методов для кодирования и декодирования массивов в encoding/gob, создания набора данных таймзон в пакете time, и тому подобное.
Пожалуйста, используйте go generate креативно. Он тут для того, чтобы поощрять эксперименты.
И даже если нет, используйте stringer, чтобы добавлять String методы к вашим числовым константам. Позвольте компьютеру делать работу за вас.