Pull to refresh
42.75
Открытая мобильная платформа
Российская мобильная доверенная ОС Аврора

Как писать кодогенераторы в Go

Reading time 14 min
Views 12K

Однообразный код писать неинтересно, нудно, но приходится. Испокон веков изворотливые программисты ищут Святой Грааль формализма, позволяющего переложить рутинные задачи на машину, писать только раз и переиспользовать код. Так появились структурное программирование, потом объектно-ориентированное, полиморфизм с параметризованными типами, кодогенерация на основе формальных грамматик, препроцессоры макроязыка и прочее…​ Под катом рассмотрим, как обстоят дела именно в Go.


В Go на сегодня generics нет (хоть третий год и обещают), а выписывать по шаблону GetMax([]MyType) для каждого MyType надоедает.

Параметрический полиморфизм можно реализовать генерацией частных форм обобщённого кода на стадии компиляции (или выполнения) и поддержкой таблиц соответствия на стадии выполнения. В Go поддерживаются таблицы методов для типов и интерфейсов и диспетчеризация этих таблиц — просто, зато эффективно реализовано. 

Runtime-доступ к диспетчеру предоставлен пакетом reflect, что обеспечивает сильный, но дорогостоящий механизм интроспекции, позволяющий динамически оперировать статически заявленными типами. Вызовы reflect затратны, но, например, в C нет и этого, там на стадии выполнения данные не знают, какого они типа.

Стандартного препроцессора в Go тоже нет. Зато есть директива go:generate и есть доступ к потрохам компилятора, в частности к дереву разбора (Abstract Syntax Tree), в пакетах go/ стандартной библиотеки. Это в совокупности даёт инструментарий богаче, чем препроцессор макросов.

Идиоматическое применение интерфейсов реализовано в stdlib-пакете sort, интроспекция применяется в пакетах encoding и fmt, go:generate — в придворном пакете golang.org/x/tools/cmd/stringer.

Манипулирование AST исходного кода не очень распространено, потому что:

  • кодогенерацию трудно верифицировать;

  • дерево разбора кажется сложным, непонятным и пугает.

Как раз на использовании AST в быту мы и остановимся.

Go- и JS-разработчик Открытой мобильной платформы Дима Смотров рассказал, как писать кодогенераторы в Go и оптимизировать работу над микросервисами с помощью создания инструмента для генерации шаблонного кода. Статья составлена на основе выступления Димы на GopherCon Russia 2020.

О продуктах и компонентах на Go

Наша команда разрабатывает мобильную ОС Аврора, SDK и экосистему приложений под неё, доверенную среду исполнения Аврора ТЕЕ, систему по управлению корпоративной мобильной инфраструктурой Аврора Центр, включающую несколько «коробочных» продуктов и компонентов.

Группа Дмитрия, в частности, работает над продуктом Аврора Маркет, который обеспечивает управление дистрибуцией приложений. Его бэкенд полностью написан на Go.

В Go принято отдавать предпочтение явному программированию (explicit) в противовес неявному (implicit). Это помогает новым разработчикам легче начинать работать над существующими проектами. Но по пути от неявного программирования к явному можно легко заблудиться и забрести в дебри дубляжа кода, а дубляж кода в дальнейшем превратит поддержку проекта в ад.

Чтобы этого избежать, код выносят в отдельные модули. Но как насчёт кода, который пишется специально для каждого микросервиса и не может быть вынесен в модуль? Например, код репозитория для работы с базой данных. Этот код есть в каждом микросервисе, выглядит примерно одинаково, но он разный и не дублируется. Не хочется писать шаблонный код, который потом придётся ещё и поддерживать во всех микросервисах.

Кодогенерация — официальный инструмент от авторов Go

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

И хотя в Go принято отдавать предпочтение явному программированию, разработчики предоставили инструменты для метапрограммирования, такие как кодогенерация ($go help generate) и Reflection API. Reflection API используется на этапе выполнения программы, кодогенерация — перед этапом компиляции. Reflection API увеличивает время работы программы. Пример: инструмент для кодирования и декодирования JSON из стандартной библиотеки Go использует Reflection API. Взамен ему сообществом были рождены такие альтернативы, как easyjson, который с помощью кодогенерации кодирует и декодирует JSON в 5 раз быстрее.

Так как кодогенерация — неявное программирование, она недооценивается сообществом Go, хотя и является официальным инструментом от создателей этого языка программирования. Поэтому в интернете немного информации о написании кодогенераторов на Go. Но всё же на Хабре примеры есть: 1 и 2.

При разработке микросервисов есть много похожего шаблонного кода, который нужно писать в каждом микросервисе. Например, код репозитория по работе с базой данных. Мы создали кодогенераторы для того, чтобы разработчики не тратили время на написание этого шаблонного кода и могли сфокусироваться на решении задач, относящихся к дизайну кода и предметной области бизнеса. Команда использует кодогенераторы и для сокращения времени на создание новых микросервисов. Это позволяет не ограничивать разработчика в принятии архитектурных решений, так как создание нового микросервиса не влияет на трудоёмкость выполнения задачи.

Пример дублирующего кода:

type UserRepository struct{ db *gorm.DB }

func NewRepository(db *gorm.DB) UserRepository {
    return UserRepository{db: db}
}

func (r UserRepository) Get(userID uint) (*User, error) {
    entity := new(User)
    err := r.db.Limit(limit: 1).Where(query: "user_id = ?", userID).Find(entity).Error
    return entity, err
}

func (r UserRepository) Create(entity *User) error {
    return r.db.Create(entity).Error
}
func (r UserRepository) Update(entity *User) error {
    return r.db.Model(entity).Update(entity).Error
}

func (r UserRepository) Delete(entity *User) error {
    return r.db.Delete(entity).Error
}

Про удачные кодогенераторы

Из примеров написанных и удачно используемых в нашей команде кодогенераторов хотим подробнее рассмотреть генератор репозитория по работе с базой данных. Нам нравится переносить опыт из одного языка программирования в другой. Так, наша команда попыталась перенести идею генерации репозиториев по работе с базой данных из Java Spring (https://spring.io/).

В Java Spring разработчик описывает интерфейс репозитория, исходя из сигнатуры метода автоматически генерируется реализация в зависимости от того, какой бэкенд для базы данных используется: MySQL, PostgreSQL или MongoDB. Например, для метода интерфейса с сигнатурой FindTop10WhereNameStartsWith (prefix string) автоматически генерируется реализация метода репозитория, которая вернёт до 10 записей из базы данных, имя которых начинается с переданного в аргументе префикса.

О нюансах и траблах внедрения кодогенератора

Существует парадигма Monolith First, когда пишут первую версию как монолит, а потом распиливают на микросервисы. На заре новой версии проекта, когда все команды должны были разбить монолит на микросервисы, мы решили написать свой генератор, который:

  • позволит вводить в систему новые микросервисы с меньшими усилиями, чем при его создании вручную (копируя предыдущий и удаляя лишнее);

  • сократит время на код-ревью за счёт общего шаблона для генерируемых микросервисов;

  • сократит время на будущие обновления одинакового кода микросервисов (main, инфрастуктура, etc…).

Для разработки микросервисов командами было принято решение использовать go-kit. За основу мы взяли один из популярных существующих кодогенераторов для go-kit и стали его дорабатывать под наши требования для микросервисов. Он был написан с использованием не очень удобной библиотеки, которая использовала промежуточные абстракции для генерации кода Go. Код получался громоздким и трудным для восприятия и поддержки. В будущих версиях мы отказались от такого подхода и начали генерировать код Go с помощью шаблонов Go. Это позволило нам писать тот же самый код без каких-либо промежуточных абстракций. За пару недель нашей командой был написан прототип. А ещё через месяц был написан кодогенератор для go-kit, который буквально умел делать всё.

Разработчик описывает интерфейс go-kit-сервиса, а кодогенератор генерирует сразу всё, что для сервиса нужно:

  • CRUD-эндпоинты и REST-, gRPC- и NATS-транспорты;

  • репозиторий для работы с базой данных с возможностью расширять интерфейс репозитория;

  • main для всех go-kit-сервисов.

После того как кодогенератор был закончен, началось его внедрение. Мы сразу же столкнулись с проблемами. Разработчики компании неохотно принимали кодогенератор. Он генерировал слишком много кода, который нужно было ревьюить и перерабатывать. Сначала команды помогали исправлять генерируемый код, но подошло время уже разрабатывать сами микросервисы, а не кодогенератор для генерации сервисов. В итоге одни команды продолжали использовать кодогенератор, обходя его баги, а другие начали писать микросервисы без кодогенератора. Мы получили сегментацию сервисов. Когда инфраструктура менялась, изменения давались нам очень тяжело. 

Позже наша команда работала над проектом, где нужно было больше заниматься адаптацией существующих Open-Source-продуктов под наши требования, чем разработкой новых микросервисов. Мы больше не могли заниматься кодогенератором, так как не использовали его активно в работе. А впоследствии мы тоже начали копировать сервисы вместе со всеми их недостатками.

Когда наша команда вернулась к разработке своего основного продукта, мы провели ретроспективу кодогенератора и поняли, в чём была основная проблема. Кодогенераторы, которые «генерируют ВСЁ», сложно внедрять и поддерживать.

  • Кодогенератор генерировал слишком много кода.

  • Весь код нужно было ревьювить и перерабатывать.

  • Только часть команд решила пользоваться кодогенератором.

  • Получили сегментацию микросервисов.

Мы исправили эту проблему. Сейчас кодогенератор разбили на несколько мелких, у каждого из которых своя собственная ответственность. Разработчики сами выбирают, какие из них использовать в своих продуктах, а какие нет.

Как же всё-таки генерировать Go-код

Можно просто использовать шаблоны. Можно написать шаблон и начинить его параметрами, на это вполне способны продвинутые редакторы текста. Можно использовать неинтерактивные редакторы — sed или awk, порог входа круче, зато лучше поддаётся автоматизации и встраивается в производственный конвейер. Можно использовать специфические инструменты рефакторинга Go из пакета golang.org/x/tools/cmd, а именно gorename или eg. А можно воспользоваться пакетом text/template из стандартной библиотеки — решение достаточно гибкое, человекочитаемое (в отличие от sed), удобно интегрируется в pipeline и позволяет оставаться в среде одного языка.

И всё же для конвейерной обработки этого маловато: требует существенного вмешательства оператора.

Можно пойти по проторённому пути: gRPC, Protobuf, Swagger. Недостатки подхода:

  • привязывает к gRPC, Protobuf;

  • не заточен конкретно под Go, а, напротив, требует изучения и внедрения новых, сторонних абстракций и технологий.

Чтобы остаться в родных пенатах воспользуемся средствами из стандартной библиотеки — пакетами go/:

  • go/ast — декларирует типы дерева разбора;

  • go/parser — разбирает исходный код в эти типы;

  • go/printer — выливает AST в файл исходного кода;

  • go/token — обеспечивает привязку дерева разбора к файлу исходного кода.

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

Можно вытащить из AST параметры, вмонтировать в шаблон и всё. Недостаток — разрозненные обработки неудобно собирать в конвейер.

Поэтому выбран такой алгоритм кодогенерации:

  1. Разбираем AST исходного файла.

  2. Создаём пустое AST для генерируемого файла.

  3. Генерируем код из шаблонов Go (template/text).

  4. Разбираем AST сгенерированного кода.

  5. Копируем узлы AST из сгенерированного кода в AST генерируемого файла.

  6. Печатаем и сохраняем AST генерируемого файла в файл.

Чтобы было понятней и не пугала загадочная аббревиатура AST — дерево разбора Hello World:

package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}

...выглядит вот так:

...или вот так, напечатанное специализированным принтером ast.Print():

ast.Print
0  *ast.File {
1  .  Package: 2:1
2  .  Name: *ast.Ident {
3  .  .  NamePos: 2:9
4  .  .  Name: "main"
5  .  }
6  .  Decls: []ast.Decl (len = 2) {
7  .  .  0: *ast.GenDecl {
8  .  .  .  TokPos: 4:1
9  .  .  .  Tok: import
10  .  .  .  Lparen: -
11  .  .  .  Specs: []ast.Spec (len = 1) {
12  .  .  .  .  0: *ast.ImportSpec {
13  .  .  .  .  .  Path: *ast.BasicLit {
14  .  .  .  .  .  .  ValuePos: 4:8
15  .  .  .  .  .  .  Kind: STRING
16  .  .  .  .  .  .  Value: "\"fmt\""
17  .  .  .  .  .  }
18  .  .  .  .  .  EndPos: -
19  .  .  .  .  }
20  .  .  .  }
21  .  .  .  Rparen: -
22  .  .  }
23  .  .  1: *ast.FuncDecl {
24  .  .  .  Name: *ast.Ident {
25  .  .  .  .  NamePos: 6:6
26  .  .  .  .  Name: "main"
27  .  .  .  .  Obj: *ast.Object {
28  .  .  .  .  .  Kind: func
29  .  .  .  .  .  Name: "main"
30  .  .  .  .  .  Decl: *(obj @ 23)
31  .  .  .  .  }
32  .  .  .  }
33  .  .  .  Type: *ast.FuncType {
34  .  .  .  .  Func: 6:1
35  .  .  .  .  Params: *ast.FieldList {
36  .  .  .  .  .  Opening: 6:10
37  .  .  .  .  .  Closing: 6:11
38  .  .  .  .  }
39  .  .  .  }
40  .  .  .  Body: *ast.BlockStmt {
41  .  .  .  .  Lbrace: 6:13
42  .  .  .  .  List: []ast.Stmt (len = 1) {
43  .  .  .  .  .  0: *ast.ExprStmt {
44  .  .  .  .  .  .  X: *ast.CallExpr {
45  .  .  .  .  .  .  .  Fun: *ast.SelectorExpr {
46  .  .  .  .  .  .  .  .  X: *ast.Ident {
47  .  .  .  .  .  .  .  .  .  NamePos: 7:2
48  .  .  .  .  .  .  .  .  .  Name: "fmt"
49  .  .  .  .  .  .  .  .  }
50  .  .  .  .  .  .  .  .  Sel: *ast.Ident {
51  .  .  .  .  .  .  .  .  .  NamePos: 7:6
52  .  .  .  .  .  .  .  .  .  Name: "Println"
53  .  .  .  .  .  .  .  .  }
54  .  .  .  .  .  .  .  }
55  .  .  .  .  .  .  .  Lparen: 7:13
56  .  .  .  .  .  .  .  Args: []ast.Expr (len = 1) {
57  .  .  .  .  .  .  .  .  0: *ast.BasicLit {
58  .  .  .  .  .  .  .  .  .  ValuePos: 7:14
59  .  .  .  .  .  .  .  .  .  Kind: STRING
60  .  .  .  .  .  .  .  .  .  Value: "\"Hello, World!\""
61  .  .  .  .  .  .  .  .  }
62  .  .  .  .  .  .  .  }
63  .  .  .  .  .  .  .  Ellipsis: -
64  .  .  .  .  .  .  .  Rparen: 7:29
65  .  .  .  .  .  .  }
66  .  .  .  .  .  }
67  .  .  .  .  }
68  .  .  .  .  Rbrace: 8:1
69  .  .  .  }
70  .  .  }
71  .  }
72  .  Scope: *ast.Scope {
73  .  .  Objects: map[string]*ast.Object (len = 1) {
74  .  .  .  "main": *(obj @ 27)
75  .  .  }
76  .  }
77  .  Imports: []*ast.ImportSpec (len = 1) {
78  .  .  0: *(obj @ 12)
79  .  }
80  .  Unresolved: []*ast.Ident (len = 1) {
81  .  .  0: *(obj @ 46)
82  .  }
83  }

Хватит трепаться, покажите код

В целом задача сводится к тому, чтобы разработчик микросервиса мог описать свои данные обычным образом в стандартном синтаксисе Go, только добавить в комментариях директивы процессинга:

//repogen:entity
type User struct {
    ID              uint `gorm:"primary_key"`
    Email           string
    PasswordHash    string
}

...запустить go generate и получить вот такой файл с готовой обвязкой для работы с DB, в котором прописаны методы именно для его типа данных User:

User
type UserRepository struct{db *gorm.DB}

func NewRepository(db *gorm.DB) UserRepository {
    return UserRepository{db: db}
}

func (r UserRepository) Get(userID uint) (*User, error) {
    entity := new(User)
    err := r.db.Limit(limit: 1).Where(query: "user_id = ?", userID).Find(entity).Error
    return entity, err
}

func (r UserRepository) Create(entity *User) error {
    return r.db.Create(entity).Error
}
func (r UserRepository) Update(entity *User) error {
    return r.db.Model(entity).Update(entity).Error
}

func (r UserRepository) Delete(entity *User) error {
    return r.db.Delete(entity).Error
}

Напишем такой генератор прямо здесь и сейчас, не применяя готовых решений, а ограничившись стандартной библиотекой.

Кода потребовалось не очень много, поэтому он представлен одним листингом, чтобы не терялась общая картина. Пояснения даны в комментариях, в стиле literate programming.

Вот модель, для которой нам нужно сгенерировать методы работы с DB. В комментариях видны директивы:

  • go:generate repogen — для команды go generate на запуск процессора repogen;

  • repogen:entity — помечает цель для процессора repogen;

  • и тег поля структуры gorm:"primary_key" для процессора gorm — помечает первичный ключ в таблице DB.

package gophercon2020

//go:generate repogen

//repogen:entity
type User struct {
    ID              uint `gorm:"primary_key"`
    Email           string
    PasswordHash    string
}

Вот код, собственно, процессора repogen:

Процессор repogen
package main

import (
    "bytes"
    "go/ast"
    "go/parser"
    "go/printer"
    "go/token"
    "golang.org/x/tools/go/ast/inspector"
    "log"
    "os"
    "text/template"
)

//Шаблон, на основе которого будем генерировать
//.EntityName, .PrimaryType — параметры,
//в которые будут установлены данные, добытые из AST-модели
var repositoryTemplate = template.Must(template.New("").Parse(`
package main

import (
    "github.com/jinzhu/gorm"
)

type {{ .EntityName }}Repository struct {
    db *gorm.DB
}

func New{{ .EntityName }}Repository(db *gorm.DB) {{ .EntityName }}Repository {
    return {{ .EntityName }}Repository{ db: db}
}

func (r {{ .EntityName }}Repository) Get({{ .PrimaryName }} {{ .PrimaryType}}) (*{{ .EntityName }}, error) {
    entity := new({{ .EntityName }})
    err := r.db.Limit(1).Where("{{ .PrimarySQLName }} = ?", {{ .PrimaryName }}).Find(entity).Error()
    return entity, err
}


func (r {{ .EntityName }}Repository) Create(entity *{{ .EntityName }}) error {
    return r.db.Create(entity).Error
}

func (r {{ .EntityName }}Repository) Update(entity *{{ .EntityName }}) error {
    return r.db.Model(entity).Update.Error
}

func (r {{ .EntityName }}Repository) Update(entity *{{ .EntityName }}) error {
    return r.db.Model(entity).Update.Error
}

func (r {{ .EntityName }}Repository) Delete(entity *{{ .EntityName }}) error {
    return r.db.Delete.Error
}
`))

//Агрегатор данных для установки параметров в шаблоне
type repositoryGenerator struct{
    typeSpec    *ast.TypeSpec
    structType  *ast.StructType
}

//Просто helper-функция для печати замысловатого ast.Expr в обычный string
func expr2string(expr ast.Expr) string {
    var buf bytes.Buffer
    err := printer.Fprint(&buf, token.NewFileSet(), expr)
    if err !- nil {
        log.Fatalf("error print expression to string: #{err}")
    return buf.String()
}

//Helper для извлечения поля структуры,
//которое станет первичным ключом в таблице DB
//Поиск поля ведётся по тегам
//Ищем то, что мы пометили gorm:"primary_key"
func (r repositoryGenerator) primaryField() (*ast.Field, error) {
    for _, field := range r.structType.Fields.List {
        if !strings.Contains(field.Tag.Value, "primary")
            continue
        }
        return field, nil
    }
    return nil, fmt.Errorf("has no primary field")
}

//Собственно, генератор
//оформлен методом структуры repositoryGenerator,
//так что параметры передавать не нужно:
//они уже аккумулированы в ресивере метода r repositoryGenerator
//Передаём ссылку на ast.File,
//в котором и окажутся плоды трудов
func (r repositoryGenerator) Generate(outFile *ast.File) error {
    //Находим первичный ключ
    primary, err := r.primaryField()
    if err != nil {
        return err
    }
    //Аллокация и установка параметров для template
    params := struct {
        EntityName      string
        PrimaryName     string
        PrimarySQLName  string
        PrimaryType     string
    }{
        //Параметры извлекаем из ресивера метода
        EntityName      r.typeSpec.Name.Name,
        PrimaryName     primary.Names[0].Name,
        PrimarySQLName  primary.Names[0].Name,
        PrimaryType     expr2string(primary.Type),
    }
    //Аллокация буфера,
    //куда будем заливать выполненный шаблон
    var buf bytes.Buffer
    //Процессинг шаблона с подготовленными параметрами
    //в подготовленный буфер
    err = repositoryTemplate.Execute(&buf, params)
    if err != nil {
        return fmt.Errorf("execute template: %v", err)
    }
    //Теперь сделаем парсинг обработанного шаблона,
    //который уже стал валидным кодом Go,
    //в дерево разбора,
    //получаем AST этого кода
    templateAst, err := parser.ParseFile(
        token.NewFileSet(),
        //Источник для парсинга лежит не в файле,
        "",
        //а в буфере
        buf.Bytes(),
        //mode парсинга, нас интересуют в основном комментарии
        parser.ParseComments,
    )
    if err != nil {
        return fmt.Errorf("parse template: %v", err)
    }
    //Добавляем декларации из полученного дерева
    //в результирующий outFile *ast.File,
    //переданный нам аргументом
    for _, decl := range templateAst.Decls {
        outFile.Decls = append(outFile.Decls, decl)
    }
    return nil
}

func main() {
    //Цель генерации передаётся переменной окружения
    path := os.Getenv("GOFILE")
    if path == "" {
        log.Fatal("GOFILE must be set")
    }
    //Разбираем целевой файл в AST
    astInFile, err := parser.ParseFile(
        token.NewFileSet(),
        path,
        src: nil,
        //Нас интересуют комментарии
        parser.ParseComments,
    )
    if err != nil {
        log.Fatalf("parse file: %v", err)
    }
    //Для выбора интересных нам деклараций
    //используем Inspector из golang.org/x/tools/go/ast/inspector
    i := inspector.New([]*ast.File{astInFile})
    //Подготовим фильтр для этого инспектора
    iFilter := []ast.Node{
        //Нас интересуют декларации
        &ast.GenDecl{},
    }
    //Выделяем список заданий генерации
    var genTasks []repositoryGenerator
    //Запускаем инспектор с подготовленным фильтром
    //и литералом фильтрующей функции
    i.Nodes(iFilter, func(node ast.Node, push bool) (proceed bool){
        genDecl := node.(*ast.GenDecl)
        //Код без комментариев не нужен,
        if genDecl.Doc == nil {
            return false
        }
        //интересуют спецификации типов,
        typeSpec, ok := genDecl.Specs[0].(*ast.TypeSpec)
        if !ok {
            return false
        }
        //а конкретно структуры
        structType, ok := typeSpec.Type.(*ast.StructType)
        if !ok {
            return false
        }
        //Из оставшегося
        for _, comment := range genDecl.Doc.List {
            switch comment.Text {
            //выделяем структуры, помеченные комментарием repogen:entity,
            case "//repogen:entity":
                //и добавляем в список заданий генерации
                genTasks = append(genTasks, repositoryGenerator{
                    typeSpec: typeSpec,
                    structType: structType,
                })
            }
        }
        return false
    })
    //Аллокация результирующего дерева разбора
    astOutFile := &ast.File{
        Name: astInFile.Name,
    }
    //Запускаем список заданий генерации
    for _, task := range genTask {
        //Для каждого задания вызываем написанный нами генератор
        //как метод этого задания
        //Сгенерированные декларации помещаются в результирующее дерево разбора
        err = task.Generate(astOutFile)
        if err != nil {
            log.Fatalf("generate: %v", err)
        }
    }
    //Подготовим файл конечного результата всей работы,
    //назовем его созвучно файлу модели, добавим только суффикс _gen
    outFile, err := os.Create(strings.TrimSuffix(path, ".go") + "_gen.go")
    if err != nil {
        log.Fatalf("create file: %v", err)
    }
    //Не забываем прибраться
    defer outFile.Close()
    //Печатаем результирующий AST в результирующий файл исходного кода
    //«Печатаем» не следует понимать буквально,
    //дерево разбора нельзя просто переписать в файл исходного кода,
    //это совершенно разные форматы
    //Мы здесь воспользуемся специализированным принтером из пакета ast/printer
    err = printer.Fprint(outFile, token.NewFileSet(), astOutFile)
    if err != nil {
        log.Fatalf("print file: %v", err)
    }
}

Подводя итоги

Работа с деревом разбора в Go не требует сверхъестественных способностей. Язык предоставляет для этого вполне годный инструментарий. Кода получилось не слишком много, и он достаточно читаем и, надеемся, понятен. Высокой эффективности здесь добиваться нет нужды, потому что всё происходит ещё до стадии компиляции и на стадии выполнения издержек не добавляет (в отличие от reflect). Важнее валидность генерации и манипуляций с AST. Кодогенерация сэкономила нам достаточно времени и сил в написании и поддержке большого массива кода, состоящего из повторяющихся паттернов (микросервисов). В целом кодогенераторы оправдали затраты на своё изготовление. Выбранный pipeline показал себя работоспособным и прижился в производственном процессе. Из стороннего опыта можем рекомендовать к использованию:

  • dst (у которого лучше разрешение импортируемых пакетов и привязка комментариев к узлам AST, чем у go/ast из stdlib).

  • kit (хороший toolkit для быстрой разработки в архитектуре микросервисов. Предлагает внятные, рациональные абстракции, методики и инструменты).

  • jennifer (полноценный кодогенератор. Но его функциональность достигнута ценой применения промежуточных абстракций, которые хлопотно обслуживать. Генерация из шаблонов text/template на деле оказалась удобней, хоть и менее универсальной, чем манипулирование непосредственно AST с использованием промежуточных абстракций. Писать, читать и править шаблоны проще).

Набор узконаправленных генераторов оказался сподручней одного универсального и в применении, и в поддержке. Здесь допустима аналогия «микросервисы vs монолит». Делитесь опытом и мнениями в комментариях.

Tags:
Hubs:
+7
Comments 6
Comments Comments 6

Articles

Information

Website
omp.ru
Registered
Founded
Employees
201–500 employees
Location
Россия
Representative