Кодогенерация в мові Go

У даній статті хотілося б розглянути деякі можливості кодогенарации в рамках мови Go, які можуть частково замінити вбудовану рефлексію і не втратити типобезопасность на етапі компіляції.
Мова програмування Go надає потужні інструменти для кодогенерации. Дуже часто Go лають за відсутність узагальнень (узагальнення) і це справді може стати проблемою. І ось тут на допомогу приходить кодогенерация яка на перший погляд досить важка для невеликих рутинних операцій, але тим не менш є досить гнучким інструментом. Вже існує деяка кількість готових бібліотек кодогенерации покривають базові потреби в узагальненнях. Це і «еталонний» stringer і більш корисні jsonenums ffjson А потужний gen і зовсім дозволяє додати в Go трохи функциональщины, в тому числі додає аналог так не хватаемого багатьом forEach для користувацьких типів. До всього іншого gen досить легко розширюється власними генераторами. На жаль gen обмежений кодогенерацией методів для конкретних типів.
Власне тему кодогенерации я вирішив торкнутися не від хорошого життя, а із за того, що зіткнувся з невеликим завданням для якої не зміг знайти іншого відповідного рішення.

Завдання наступна, є список констант:
type int Color

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 AUTOMATICALLY GENERATED
//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
String Name
String List
}{
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 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.