Кодогенерация в языке Go

В данной статье хотелось бы рассмотреть некоторые возможности кодогенарации в рамках языка Go, которые могут частично заменить встроенную рефлексию и не потерять типобезопасность на этапе компиляции.
Язык программирования 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.
обход ast
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.

Комментарии (0)

© Habrahabr.ru