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

Original author: Rob Pike
  • Translation
Перевод статьи Роба Пайка из официального блога 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 методы к вашим числовым константам. Позвольте компьютеру делать работу за вас.
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 26

    +6
    Вот бы go generate вызывался бы автоматически, при изменении зависимых файлов.
    Короче хочу make
      +1
      Ну Make вам никто не мешает использовать. До go generate, в основном, подобная задача решалась именно мейкфайлами, да и сейчас, некоторые их, по привычке, используют с Go. Хотя большая часть проектов, безусловно, стараются, чтобы workflow был стандартным — go generate/build/test.

      И, как написано в статье, если вы «заставите» всех пользователей вашего пакета регенерировать файлы на каждом билде — с большой вероятностью, они получат ошибку, так как у них могут быть не установлены нужная программа-генератор. Все таки, это функция автора(ов) пакета.
      +18
      Т.к. кто-нибудь должен будет написать коммент, что программирование в комментах — это плохо, то это сделаю я.
      Но я ещё более радикален, не приемлю в принципе зависимость логики программы от форматирования файла с исходным кодом. Поэтому, для меня кодогенерация из комментов и вложенность блоков по табуляции кажутся одинаково опасными штуками.
        +6
        С табуляцией всё не так страшно, поскольку вы её в любом случае ставите, даже в языках, где она ни на что не влияет.

        В целом, имею положительное отношение к Go, но подобные моменты, типа магических комментариев, макросов, прикрученных сбоку (go generate), отсутсвие (ненужных, ага) генериков, немного огорчают. К тому же, могли бы добавить пилюлю от NPE, в 21-м веке-то (здесь нравится подход Kotlin с Int и Int?).
          –2
          В том-то и дело, что они лишь кажутся такими, потому что вы смотрите с перспективы других языков. Я полностью согласен, что есть хорошие и плохие практики, и «специальных комментарии» — одна из как-бы не очень хороших практик, хотя я сходу не вспомню, где это действительно приводило к проблемам.

          Но возьмите в пример такую вещь, как shebang — казалось бы, тоже самое. Но, как показала практика многих десятков лет — это удобное и рабочее решение.

          Аналогично и с go generate — мне не встречался ни один случай, где факт того, что go generate описывается комментариев приводил к какой-либо проблеме или путанице. Если вы пишете на Go и для вас это стало проблемой — расскажите, интересно же.
          По факту, это практично и удобно, это обратно совместимо и не приводит к непоняткам.

          Так что, если не вешать раньше времени ярлыки, а попробовать в деле — то всё окажется не таким, как кажется поначалу.
            +3
            > и «специальных комментарии» — одна из как-бы не очень хороших практик
            Называйте вещи своими именами. Это просто _плохая практика_.
              0
              Называйте вещи своими именами. Это просто _плохая практика_.

              Это здорово, что вы знаете что такое «плохие практики». И уверен, вы не слепо навешиваете ярлыки, без понимания их сути.
              Расскажите, пожалуйста, какие проблемы ждут Go-программистов из-за такого дизайна go generate?
                +2
                Смешение понятий не сразу проявляется явными проблемами. Сначала комментарии становятся не совсем комментариями и все это принимают как должное. А потом никто уже не удивляется, что подобных «исключений» накапливается столько, что назвать это чистым дизайном просто язык не повернётся.
                Если go generate так уж нужно конфигурировать из мета-информации, то это должна быть мета-информация со своим явным синтаксисом. А комментарии оставьте тем, чем они и должны являться.
                  +2
                  Знаете, что мне нравится в Go, так это то, что я могу быть уверен, что его ядром занимаются люди в разы умнее и опытнее меня. Я бы себя посчитал крайне наивным, если бы стал заявлять, что я «вижу очевидный минус, который авторы Go просто не заметили или не додумались». Мне это, кстати, помогло очень быстро пройти эти этапы «WTF, да какое право они имеют мне говорить, что неиспользуемый импорт — это ошибка!». И я очень благодарен такой «радикальности» многих решений Go — они, за меня, заставили меня писать код лучше и чище, не игнорировать ошибки, писать тесты и так далее. Зная, что именно так и задумывалось, я считаю это бесценным — умение смотреть далеко вперед и помогать программистам стать лучше с помощью дизайна языка и тулинга.

                  К чему это всё я. К тому, что если появится надобность дописать ещё что-то в «специальные комментарии», а уж тем более, если не раз — то и авторы Go и коммьюнити — в котором тоже немало очень толковых людей — среагируют, обсудят и предложат более оптимальное решение, найдя наиболее правильный компромисс. С существующей системой пропозалов и open-source модели Go такой вклад может сделать любой, вобщем-то — авторы Go лишь будут следить, чтобы изменения следовали основной идее Go. Вобщем, чего-чего, а мысль о том, что в Go в комментариях появится каша из всего чего угодно, и всем будет на это наплевать — я не верю. Слишком хорошо знаю людей, которые за этим стоят — в том числе, по личному общению на конференциях.
                    +2
                    Я потому где-то уже и писал, что все же считаю это временным решением, которое однажды перетечёт в другую форму (и придётся поддерживать обратную совместимость). Или не перетечёт, т.к. уже много всего будет написано именно так (привет, пхп).
                    Не хочу снова холиварить насчёт неиспользуемого импорта. Конечно, IDE могут научиться сами комментировать импорт, если закомментирован код отладки. Что прибивает разработчика гвоздями к конкретной IDE.
                    В общем итоге, никто не пытается выставить Go в свете какой-то непроработанности (точно уж не я). Но я призываю поддерживать тех, кто вытягивает всяческие кривульки из тени под лампы прожекторов и говорит ясно и честно: это не лучшее решение.
                      +2
                      Не хочу снова холиварить насчёт неиспользуемого импорта.

                      Да нечего тут холиварить — это исключительно привычка. Вы давно на Go пишете? Просто у практически всех эта фича становится любимой — она *гарантирует*, что у вас не будет кучи лишних импортов, вы уверены, что их не будет ни в чьем коде и т.д. — это big deal на самом деле, ворнинги такой эффект не дадут. А во время отладки поставить _ или удалить импорт — оказывается совсем несложно, особенно, если используется goimports, который сам добавляет/удаляет импорты и его вешают на save-хук, вместо go fmt (он drop-in замена go fmt по сути).

                      Но я призываю поддерживать тех, кто вытягивает всяческие кривульки из тени под лампы прожекторов и говорит ясно и честно: это не лучшее решение.

                      Тут я только за. Но за объективную дискуссию, а не за «ну это же очевидно, что это плохо, потому что плохо» :) А если уж и обсуждать дизайн, то это должны делать люди, которые, как минимум, знакомы с Go.
                      Я, кстати, тоже думаю, что если бы потребность в go generate заложили до релиза 1.0, то, возможно, было бы иное решение (хотя мне сложно предположить — какое, нынешний дизайн go generate реально всех устраивает и отлично выполняет свою функцию). Но один из посылов дизайна было — «it must fit well with the existing go command», и это, конечно, важный фактор.
                        0
                        Или можно пойти по пути джавы, которая сама удаляет неиспользуемые импорты при компиляции, но это потеря скорости компиляции, которой go заслуженно гордится.
                  +1
                  Кстати, хочу отдать вам должное. Вы принимаете существенные усилия для популяризации и защиты Go в русскоязычном сообществе, хотя он и сам по себе достаточно заметный язык.
                    +1
                    Спасибо. Комментаторы на хабре, конечно, иногда отбивают это желание, но я, все-таки, получаю некоторое удовольствие — переводить мне легко, идею Go я очень хорошо чувствую нутром (она совпадает с моими собственными взглядами), плюс переводы заставляют внимательней углубляться даже в темы, которые, казалось бы я для себя давным давно уже закрыл. Вобщем, сплошная польза ) А если ещё и кому-то это полезней (не все ж, наверное, англоязычные ресурсы легко читают), то тем более хорошо.
                +2
                А в чём преимущества "//" перед каким-нибудь другим символом? Каковы причины использования «магических комментариев» а не отдельного синтаксиса?
                  +1
                  Я так понимаю, отдельный синтаксис/сущность — это усложнение грамматики, усложнение языка, лишнее и не нужное. Его плюсы никак не перевешивают минусов. Плюс нарушается прямая совместимость — код «с новым синтаксисом» нельзя будет собрать на предыдущей версии (это не критично, гарантируется только обратная совместимость, но всё же, если можно этого избежать, лучше избегать).
                  Кроме того, специальные комментарии в Go давно использовались, и выполняют свою функцию отлично, никаких проблем с ними нет.
                    +2
                    Лишняя сущность уже есть. А использовать для нее существующий синтаксис, это все равно использовать для умножения плюс с особым синтаксисом. Не считаю что это упрощает язык. Нарушения прямой совместимости в развивающемся языке это не страшно ИМХО.
                    0
                    В том, что обработкой этих специальных комментариев и генерацией кода занимается вовсе не компилятор, а отдельная утилита. Поэтому для компилятора это не является какой-либо синтаксической конструкцией, от которой зависит результат компиляции. На момент запуска компилятора весь код уже должен быть сгенерирован.
                      +3
                      Ну тогда берем любой символ — например '@'. Не знаком с синтаксисом go слишком уж основательно, потому предположу, что этот символ еще никак не задействован. И пусть компилятор воспринимает строки начинающиеся с этого символа как комментарии — как #pragma в C++, после которой может в принципе быть написано что угодно.
                      Какие у этого подхода минусы, кроме сломанной прямой совместимости?
                        –1
                        Очевидный минус вы сами назвали — код с таким синтаксисом несовместим со старыми версиями компилятора, а на данный момент в go кодогенерация полностью независима от процесса компиляциии и работает для всех версий go (например, если я использую go 1.0 и установлю пакет, разработчик которого использовал go generate доступный с go 1.4, то проблем не будет).

                        "#pragma" в C++ всё-таки обрабатывается на стадии препроцессинга, который является одним из этапов компиляции. А go generate полностью отделён от компиляции и является внешним по отношению к языку инструментом.
                          +3
                          Раз уж мы засовываем директивы внешнего инструмента прямо в код, то пусть компилятор их учитывает.
                          Альтернатива — отдельный файл GENERATE например с содержанием по типу:
                          stringer -type=Pill
                            0
                            Ну вы же и сами видите в каждом вами предложенном варианте минусы, правда?
                              0
                              ИМХО — внешняя утилита «полностью отдельная от компиляции» должна иметь свои отдельные файлы.
                              А если по каким то причинам мы не хотим делать так, а хотим чтобы добавить новую сущность в файлы исходников, то нежелание при этом менять компилятор выглядит странно. Если уж добавляем новую сущность в исходники, то изменение при этом компилятора никак не может считаться минусом — это необходимость.

                              Можно было закодировать go generate whitespace'ами — их ведь тоже компилятор не учитывает. Но это костыль, как и магические комментарии, и вставлять его на относительно раннем этапе развития языка довольно странно.
                                0
                                То что добавлено в компилятор должно там оставаться, а тут какие-то магические паттерны (как тут верно замечали go generate просто ищет подстроки), и внешняя тулза, может и потеряться в следующей мажорной версии.
                                А серьезно — не стоит делать чтобы компилятор сам запускал какие-то программы на неограниченном кол-ве пакетов (go generate я напоминаю надо запускать только в своих), там какой нить шутник может и rm -rf написать.
                                Так что правильно критиковать наличение go generate как такового.
                                Давайте посмотрим теперь на пример, что предлагает Пайк:

                                //go:generate go tool yacc -o gopher.go -p parser gopher.y
                                ну тут понятно, есть грамматика, создаем парсер, ок.

                                //go:generate stringer -type=Pill
                                то есть по сути попробовали добавить штуку, чтобы более менее одинаково люди писали свои обобщенно-программируемые кодогенерации, а не так например:
                                // +gen stringer
                                type Pill strunct {

                                }
                +4
                Каждый раз, когда мы меняем определение Pill или констант, всё что мы должны сделать, это запустить $ go generate
                Почему это нужно делать вручную? Это так задумано или просто недоработка?

                Второй вопрос можно легко предвосхитить: почему это было сделано через комментарии? Комментарий может быть где угодно в коде? А что будет если написать такой комментарий несколько раз в разных местах программы? Атрибуты (как в Rust) выглядят красивее как минимум (не знаю, насколько такое применимо и оправдано в случае Go, но активные комментарии выглядят всё равно странно).
                  0
                  В Go специальные комментарии были с самого начала — для тегов сборки. Мне не известно о каких-либо проблемах с этим, напротив — подход convention over configuration себя показал в Go с хорошей стороны. Видимо поэтому, решили для go generate и не плодить новые сущности и фичи языка, а воспользоваться проверенным и удобным способом.

                Only users with full accounts can post comments. Log in, please.