Язык программирования Go предоставляет мощные инструменты для кодогенерации. Очень часто Go ругают за отсутствие обобщений (generics) и это в самом деле может стать проблемой. И вот тут на помощь приходит кодогенерация которая на первый взгляд довольно трудна для небольших рутинных операций, но тем не менее является достаточно гибким инструментом. Уже существует некоторое количество готовых библиотек кодогенерации покрывающих базовые потребности в обобщениях. Это и «эталонный» stringer и более полезные jsonenums с ffjson А мощный gen и вовсе позволяет добавить в Go немного функциональщины, в том числе добавляет аналог так не хватаемого многим forEach для пользовательских типов. Ко всему прочему gen довольно легко расширяется собственными генераторами. К сожалению gen ограничен кодогенерацией методов для конкретных типов.
Собственно тему кодогенерации я решил затронуть не от хорошей жизни, а из за того, что столкнулся с небольшой задачей для которой не смог найти другого подходящего решения.
Задача следующая, есть список констант:
type Color int
const (
Green Color = iota
Red
Blue
Black
)
Необходимо иметь массив (список) содержащий в себе все константы Color, например для вывода в палитре.
Colors = [...]Color{Green, Red, Blue, Black}
При этом хочется что бы Colors формировался автоматически, дабы исключить возможность забыть добавить или удалить элемент при изменении количества констант имеющих тип Color.
Ключевыми инструментами будут следующие стандартные пакеты:
go/ast/
go/parser/
go/token/
С помощью этих пакетов мы имеем возможность получить ast (abstract syntax tree) любого файла с исходным кодом на языке go. AST получаем буквально в две строки:
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "", []byte(source), 0)
В качестве аргументов для ParseFile можно передать либо путь до файла, либо текстовое содержимое (подробности в https://golang.org/pkg/go/parser/#ParseFile). Теперь в переменной f будет содержаться ast который можно использовать для генерации необходимого кода.
Для того, что бы создать список содержащий все константы заданного типа (Color) необходимо пройтись по ast и найти узлы описывающие константы. Делается это достаточно тривиальным способом, хотя и не без особенностей. Дело в том, что Go позволяет определять не типизированные константы или список констант с авто инкрементом через конструкцию iota Для таких констант их тип в ast будет не определен, значение и тип вычисляется уже на этапе компиляции. Поэтому придется учесть особенности синтаксиса при разборе ast.
Ниже пример кода учитывающий определение констант через iota.
typeName := "Color" //тип констант для которых будет создан список
typ := "" //для запоминания последнего определенного типа в ast
consts := make([]string, 0) //массив для сохранения найденных констант
for _, decl := range f.Decls {
//массив с определениями типов, переменных, констант, функций и т.п.
switch decl := decl.(type) {
case *ast.GenDecl:
switch decl.Tok {
case token.CONST: //нам интересны только константы
for _, spec := range decl.Specs {
vspec := spec.(*ast.ValueSpec) //отсюда мы получим название константы
if vspec.Type == nil && len(vspec.Values) > 0 {
//случай определения константы как "X = 1"
//такая константа не имеет типа и может быть пропущена
//к тому же это может означать, что был начат новый блок определения const
typ = ""
continue
}
if vspec.Type != nil {
//"const Green Color" - запоминаем тип константы
if ident, ok := vspec.Type.(*ast.Ident); ok {
typ = ident.Name
} else {
continue
}
}
if typ == typeName {
//тип константы совпадает с искомым, запоминаем имя константы в массив consts
consts = append(consts, vspec.Names[0].Name)
}
}
}
}
}
Более подробно аналогичный код прокомментирован в пакете stringer.
Теперь осталось сгенерировать функцию которая вернет список из всех существующих Color.
var constListTmpl = `//CODE GENERATED AUTOMATICALLY
//THIS FILE SHOULD NOT BE EDITED BY HAND
package {{.Package}}
type {{.Name}}s []{{.Name}}
func (c {{.Name}}s)List() []{{.Name}} {
return []{{.Name}}{{"{"}}{{.List}}{{"}"}}
}
`
templateData := struct {
Package string
Name string
List string
}{
Package: "main",
Name: typeName,
List: strings.Join(consts, ", "),
}
t := template.Must(template.New("const-list").Parse(constListTmpl))
if err := t.Execute(os.Stdout, templateData); err != nil {
fmt.Println(err)
}
На выходе получим такую функцию:
type Colors []Color
func (c Colors)List() []Color {
return []Color{Green, Red, Blue, Black}
}
Использование функции:
Colors{}.List()
Листинг примера https://play.golang.org/p/Mck9Y66Z1b
Готовый к использованию генератор const_list на основе генератора stringer.
Комментарии (11)
VGrabko
29.07.2016 12:46хмм. https://habrahabr.ru/post/269887/
ZurgInq
29.07.2016 13:19Это больше похоже на перевод статьи из официального блога https://blog.golang.org/generate
ZurgInq
29.07.2016 13:31Более подробно разверну свою мысль — в указанной статье описание использование утилиты go generate и обзор возможности кодогенерации. Это хорошая статья, в которой в частности разобрана утилита stringer (именно поэтому я назвал её «эталонной»). Я же решил показать небольшой пример как достаточно просто и быстро написать свой генератор кода на основе разбора ast. В данном случае одно дополняет другое.
snuk182
29.07.2016 14:34+2Достаточно понятно написано, спасибо.
Остался только небольшой вопрос — как это все вызывается в реальном мире? Видел в коде костыли типа предложенного в официальном мануале:
//go:generate go tool yacc -o gopher.go -p parser gopher.y
но там не очень понятна связь генератора с тем местом, где а) он будет вызываться, б) где генерируемое будет использоваться.tyderh
01.08.2016 08:24А) вызываться будет в директории пакета при запуске утилиты go generate б) в вашем примере сгенерированный код будет просто положен в файл gopher.go (с первой строчкой package parser), дальше сборка идёт как обычно
mibori
29.07.2016 15:34Идея хороша, только выглядит чуть менее прозрачно, чем можно было бы.
Смысл того, что делается — автоматическая генерация конструктора. И весьма костыльно выглядит рождение коллекции объектов одного типа по методу у рандомного объекта этого типа.
То есть вместо
Colors{}.List()
более понятно для незнакомого с кодом человека генерировать обычную функциюListOfAllColors()
Хотя, дело вкуса, наверное.
leventov
29.07.2016 17:51+1А если при добавлении константы забыть вызвать генератор? Чем это лучше, чем забыть обновить вручную написанную функцию? Или генерация автоматически происходит?
ZurgInq
29.07.2016 18:38Запускать можно вручную или через go generate. Забыть запустить генератор можно, но это отчасти решается скриптом вида go generate && go build. Если есть отдельная система сборки/деплоя/ci то генерация прописывается туда наряду например с запуском тестов.
leventov
29.07.2016 18:54+1Всегда писать go generate && go build, с учетом того, что в 99% запусков ничего перегенерировать не надо? Разработчики будут забивать, ввиду бессмысленности, что таки чревато.
Гошная кодогенерация разве поощряется на уровне сборки/ci? Вроде же все пишут что надо сгенерированное коммитить в vcs.
ZurgInq
29.07.2016 19:41Насчет ci вы правы, действительно я тут погорячился. go generate && go build можно положить в условный make.sh. Суть в том, что в любом случае надо постоянно запускать такие инструменты как например go fmt, go test, go vet. К ним добавляется еще go generate при условии использования генераторов.
Возможно в дальнейшем автозапуск go generate добавят например на уровне плагинов, например idea при коммите в vcs предварительно автоматически умеет запускать go fmt.
KvanTTT
А AST нельзя обходить с помощью Visitor или Listener?