В данной статье хотелось бы рассмотреть некоторые возможности кодогенарации в рамках языка 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.
Поделиться с друзьями
-->

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


  1. KvanTTT
    29.07.2016 12:17
    +1

    А AST нельзя обходить с помощью Visitor или Listener?


  1. VGrabko
    29.07.2016 12:46

    хмм. https://habrahabr.ru/post/269887/


    1. ZurgInq
      29.07.2016 13:19

      Это больше похоже на перевод статьи из официального блога https://blog.golang.org/generate


    1. ZurgInq
      29.07.2016 13:31

      Более подробно разверну свою мысль — в указанной статье описание использование утилиты go generate и обзор возможности кодогенерации. Это хорошая статья, в которой в частности разобрана утилита stringer (именно поэтому я назвал её «эталонной»). Я же решил показать небольшой пример как достаточно просто и быстро написать свой генератор кода на основе разбора ast. В данном случае одно дополняет другое.


  1. snuk182
    29.07.2016 14:34
    +2

    Достаточно понятно написано, спасибо.
    Остался только небольшой вопрос — как это все вызывается в реальном мире? Видел в коде костыли типа предложенного в официальном мануале:

    //go:generate go tool yacc -o gopher.go -p parser gopher.y
    

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


    1. tyderh
      01.08.2016 08:24

      А) вызываться будет в директории пакета при запуске утилиты go generate б) в вашем примере сгенерированный код будет просто положен в файл gopher.go (с первой строчкой package parser), дальше сборка идёт как обычно


  1. mibori
    29.07.2016 15:34

    Идея хороша, только выглядит чуть менее прозрачно, чем можно было бы.
    Смысл того, что делается — автоматическая генерация конструктора. И весьма костыльно выглядит рождение коллекции объектов одного типа по методу у рандомного объекта этого типа.


    То есть вместо Colors{}.List() более понятно для незнакомого с кодом человека генерировать обычную функцию ListOfAllColors()


    Хотя, дело вкуса, наверное.


  1. leventov
    29.07.2016 17:51
    +1

    А если при добавлении константы забыть вызвать генератор? Чем это лучше, чем забыть обновить вручную написанную функцию? Или генерация автоматически происходит?


    1. ZurgInq
      29.07.2016 18:38

      Запускать можно вручную или через go generate. Забыть запустить генератор можно, но это отчасти решается скриптом вида go generate && go build. Если есть отдельная система сборки/деплоя/ci то генерация прописывается туда наряду например с запуском тестов.


      1. leventov
        29.07.2016 18:54
        +1

        Всегда писать go generate && go build, с учетом того, что в 99% запусков ничего перегенерировать не надо? Разработчики будут забивать, ввиду бессмысленности, что таки чревато.


        Гошная кодогенерация разве поощряется на уровне сборки/ci? Вроде же все пишут что надо сгенерированное коммитить в vcs.


        1. 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.