The Go Blog

Генерация кода

Rob Pike
22 December 2014

Свойство универсальной вычислительной мощности — полноты по Тьюрингу — заключается в том, что компьютерная программа может создавать компьютерные программы. Это мощная идея, которая не всегда получает должного признания, несмотря на то, что она происходит довольно часто. Это большая часть определения компилятора, например. Также именно так работает команда go test: она сканирует пакеты, подлежащие тестированию, создаёт Go-программу, содержащую тестовый каркас, адаптированный для пакета, а затем компилирует и запускает её. Современные компьютеры настолько быстры, что эта, казалось бы дорогая по времени последовательность может завершиться за долю секунды.

Существует множество других примеров программ, которые пишут программы. Например, Yacc читает описание грамматики и записывает программу для её разбора. «Компилятор» protocol buffer читает описание интерфейса и генерирует определения структур, методы и другой вспомогательный код. Инструменты конфигурации всех видов также работают таким образом: они анализируют метаданные или окружение и генерируют каркас, адаптированный под локальное состояние.

Таким образом, программы, которые пишут программы, являются важными элементами в инженерии программного обеспечения, но такие программы, как Yacc, которые производят исходный код, должны быть интегрированы в процесс сборки, чтобы их вывод можно было скомпилировать. Когда используется внешний инструмент сборки, такой как Make, это обычно легко сделать. Но в Go, где инструмент go получает всю необходимую информацию о сборке из исходного кода Go, возникает проблема. Просто нет механизма запуска Yacc исключительно через инструмент go.

До недавнего времени это было невозможно.

Последний выпуск Go, 1.4, включает новую команду, которая облегчает запуск таких инструментов. Она называется go generate и работает следующим образом: она сканирует специальные комментарии в исходном коде Go, которые указывают общие команды для запуска. Важно понимать, что go generate не является частью go build. Она не содержит анализа зависимостей и должна быть запущена явно перед выполнением go build. Она предназначена для использования автором Go-пакета, а не его клиентами.

Команда go generate проста в использовании. В качестве разминки, вот как можно использовать её для генерации грамматики Yacc.

Сначала установите инструмент Yacc для Go:

<code>go get golang.org/x/tools/cmd/goyacc
</code>

Допустим, у вас есть входной файл Yacc с именем gopher.y, который определяет грамматику для вашего нового языка. Чтобы получить Go-исходный файл, реализующий эту грамматику, вы обычно вызываете команду следующим образом:

<code>goyacc -o gopher.go -p parser gopher.y
</code>

Опция -o задаёт имя выходного файла, а опция -p указывает имя пакета.

Чтобы go generate запускал процесс, в любом из обычных (не сгенерированных) файлов .go в той же директории добавьте следующий комментарий в любом месте файла:

<code>//go:generate goyacc -o gopher.go -p parser gopher.y
</code>

Этот текст — просто команда выше, предваряемая специальным комментарием, который распознаётся go generate. Комментарий должен начинаться с начала строки и не иметь пробелов между // и go:generate. После этой метки, остальная часть строки задаёт команду, которую должен выполнить go generate.

Теперь выполните её. Перейдите в директорию с исходным кодом и выполните go generate, затем go build и так далее:

<code>$ cd $GOPATH/myrepo/gopher
$ go generate
$ go build
$ go test
</code>

Всё. Предполагая, что ошибок нет, команда 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. Она автоматически создаёт строковые методы для наборов целочисленных констант. Она не входит в состав официального релиза, но легко устанавливается:

<code>$ go get golang.org/x/tools/cmd/stringer
</code>

Вот пример из документации к stringer. Представьте, что у нас есть код, содержащий набор целочисленных констант, определяющих различные типы таблеток:

<code>package painkiller
type Pill int
const (
  Placebo Pill = iota
  Aspirin
  Ibuprofen
  Paracetamol
  Acetaminophen = Paracetamol
)
</code>

Для отладки мы хотим, чтобы эти константы выводились читаемо, то есть хотим метод со следующей сигнатурой:

<code>func (p Pill) String() string
</code>

Написать такой метод вручную несложно, например так:

<code>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)
        }
        </code>

Конечно, существуют и другие способы реализации этой функции. Можно использовать срез строк, проиндексированный по Pill, или карту, или какой-либо другой подход. Каким бы ни был способ, его нужно будет поддерживать при изменении набора таблеток, и нужно убедиться, что он корректен. (Два названия для парацетамола делают задачу чуть более сложной, чем могла бы быть.) Кроме того, выбор подхода зависит от типов и значений: со знаком или без, плотный или разреженный, начиная с нуля или нет и так далее.

Программа stringer заботится обо всех этих деталях. Хотя она может запускаться отдельно, она предназначена для использования с помощью команды go generate. Чтобы использовать её, добавьте комментарий generate в исходный код, возможно, рядом с определением типа:

<code>//go:generate stringer -type=Pill
</code>

Это указание говорит go generate, что нужно запустить инструмент stringer для генерации метода String для типа Pill. Результат автоматически записывается в файл pill_string.go (это значение по умолчанию, которое можно изменить с помощью флага -output).

Запустим её:

<code>$ go generate
$ cat pill_string.go
// Code generated by stringer -type Pill pill.go; DO NOT EDIT.
package painkiller
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]]
}
$
</code>

Каждый раз, когда изменяется определение Pill или константы, достаточно выполнить команду

<code>$ go generate
</code>

для обновления метода String. И конечно, если у вас несколько типов настроено таким образом в одном пакете, одна команда обновит все их методы String за один раз.

Нет сомнений, что сгенерированный метод выглядит некрасиво. Это нормально, ведь человеку не нужно его изменять; машинный код часто выглядит громоздко. Он работает ради эффективности. Все имена объединены в одну строку, что экономит память (только одна заголовочная строка для всех имён, даже если их миллионы). Затем массив _Pill_index сопоставляет значение с именем с помощью простого и эффективного метода. Обратите внимание, что _Pill_index — это массив (не срез; один заголовок убран), элементы которого имеют тип uint8, наименьшее целое число, достаточное для охвата всех значений. Если бы значений было больше или были отрицательные, тип _Pill_index мог бы измениться на uint16 или int8: в зависимости от того, что окажется наиболее эффективным.

Подход, используемый методами, выводимыми утилитой stringer, зависит от свойств набора констант. Например, если константы разрежены, может использоваться карта. Ниже приведён тривиальный пример на основе набора констант, представляющих степени двойки:

<code>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)
}
</code>

Короче говоря, автоматическое создание метода позволяет нам сделать лучше, чем можно было бы ожидать от человека.

В дереве Go уже установлено множество других применений команды go generate. Примеры включают генерацию таблиц Юникода в пакете unicode, создание эффективных методов для кодирования и декодирования массивов в пакете encoding/gob, производство данных часовых поясов в пакете time и так далее.

Пожалуйста, используйте go generate творчески. Она предназначена для поощрения экспериментов.

И даже если вы не будете использовать её, воспользуйтесь новой утилитой stringer для написания методов String для целочисленных констант. Пусть машина делает всю работу.

Следующая статья: The Gopher Gala — первая мировая хакатон-конкурс по Go
Предыдущая статья: Выпущен Go 1.4
Индекс блога

GoRu.dev Golang на русском

На сайте представлена адаптированная под русский язык документация языка программирования Golang