Доброго времени суток, дорогой читатель! На этот раз публикую перевод статьи - https://betterprogramming.pub/how-to-generate-code-using-golang-templates-942cba2e5e0c
Важно: Авто блога, не просто переводит статьи, а повторяет все действия руками, корректирую код если есть необходимость, гарантируя, что решение представленное в статье рабочее.
Основной мотивацией автора статьи, было изучение возможностей генерации когда Golang ну и конечно собственное развитие. Ваш покорный слуга, преследуют те-же цели, изучая статьи зарубежных авторов, прорабатывая их шаг за шагом и переводя их для вас.
Начинаем
Код который вы найдете в этой статье вдохновлен и частично основан на реализации другого автора, которую вы можете посмотреть в видео ролике от Данальда Фьюри.
Примечание от переводчика: В следующей статье, мы детально разберем решение от Дональда Фьюри и повторим его.
Вот только заместо генерации структур на основе АПИ, как сделано у Дональда Фьюри, я пошел путем реализации шаблона проектирования "Строитель", подробнее про который можно почитать в этой статье.
В этой статье я не буду объяснять, как работает шаблон проектирования "Строитель". Для объяснения я рекомендую вам ознакомиться со статьей, упомянутой выше, а также прочитать классическую книгу Gamma и др. “Шаблоны проектирования — элементы многоразового объектно-ориентированного программного обеспечения”.
Код который будет сгенерирован, будет иметь небольшие отличия так как, не все может быть сгенерировано.
В момент генерации, код зависит от тех данных, на основе которых будет проходить генерация и для этого эксперимента мы будем использовать следующие данные:
- Структура ProductTarget. Имя этой структуры мы будем задавать и в данном примере ее имя будет house
- N свойств у структуры ProductTarget, где N > 0. Мы будем задавать имена и типы свойств структуры в виде пар ключ-значение. В качестве примера используем: windowType-string, doorType-string, and floors-int.
- N конкретных имен товаров, где N > 0. Здесь мы будем сообщать категории наших продуктов, например normal и igloo
Далее, мы будем рассматривать логику парсинга и генерации шаблонов, сами же шаблоны мы рассмотрим позже.
Go приложение, отвечающее за генерацию.
package main
import (
"bufio"
"bytes"
"fmt"
"github.com/Masterminds/sprig"
"go/format"
"html/template"
"log"
"os"
)
type Data struct {
ProductTarget string
ConcreteTargets []string
Properties []Property
}
type Property struct {
Name string
TypeName string
}
func main() {
data := Data{
ProductTarget: "house",
ConcreteTargets: []string{"normal", "igloo"},
Properties: []Property{
{"windowType", "string"},
{"doorType", "string"},
{"floor", "int"},
},
}
processTemplate("iBuilder.tmpl", "iBuilder.go", data)
processTemplate("ProductTarget.tmpl", data.ProductTarget+".go", data)
processTemplate("director.tmpl", "director.go", data)
processTemplate("sample_main.tmpl", "sample_main.go", data)
processConcreteTargets("ConcreteTarget.tmpl", data)
fmt.Println("Remember to edit the files that contain the Concrete Targets!")
}
func processTemplate(fileName string, outputFile string, data Data) {
tmpl := template.Must(template.New("").Funcs(sprig.FuncMap()).ParseFiles(fileName))
var processed bytes.Buffer
err := tmpl.ExecuteTemplate(&processed, fileName, data)
if err != nil {
log.Fatalf("Unable to parse data into template: %v\n", err)
}
formatted, err := format.Source(processed.Bytes())
if err != nil {
log.Fatalf("Could not format processed template: %v\n", err)
}
outputPath := "./tmp/" + outputFile
fmt.Println("Writing file: ", outputPath)
f, _ := os.Create(outputPath)
w := bufio.NewWriter(f)
w.WriteString(string(formatted))
w.Flush()
}
func processConcreteTargets(fileName string, data Data) {
for _, value := range data.ConcreteTargets {
newData := data
newData.ConcreteTargets = []string{value}
processTemplate(fileName, value+".go", newData)
}
}
Далее, давайте разберем с вами, как работает этот код. Строки 3-12 импортируют пакеты, с которыми предстоит работать. На что тут стоит обратить внимание:
- bufio - для записи сгенерированных файлов на диск
- github.com/Masterminds/sprig - предоставляет удобный функционал для работы с шаблонами (Go Templates)
- go/format - для форматирования кода после генерации в формате gofmt
- html/template - для работы со стандартными функциями форматирования, есть еще text/template но он не работает корректно совместно с github.com/Masterminds/sprig
На строках 26-34 были заданы первичные данные, которые в дальнейшем будут использоваться для генерации кода.
На строках 35-39 мы устанавливает процессинг для каждого шаблона по отдельности, вызывая функции processTemplate() и processConcreteTargets().
Примечание от переводчика: на самом деле, код можно было бы упростить, если задать имена файлов в виде slice или map и в дальнейшем по циклу просто провести инициализацию
Дальше мы рассмотрим функцию, которая выполняет самую тяжелую работу, а именно processTemplate()
На 44 строке, мы вызываем функцию template.Must(), эта функция проверяет, что представленный ей шаблон написан корректно, все скобки закрываются и.т.д
В строке 46 вместо Execute() используется функция ExecuteTemplate(), чтобы избежать появления сообщения об ошибке.
В строке 50 вызываем format.Source() (из пакета go/format), это гарантирует, что вывод ExecuteTemplate() отформатирован в соответствии с принятым стилем “gofmt”.
В строке 54 мы устанавливаем выходную директорию, ту директорию куда будет сгенерирован наш код. В гашем случае, это директория ./tmp, однако, вы можете поменять ее по своему усмотрению.
На строках 56-59 мы сохраняем отрендеренные файлы в выходную директорию.
Функция processConcreteTargets() является вспомогательной и помогает сгенерировать файлы для каждой категории продукта.
На 64 строке мы создаем копию объекта data.
На строке 65 мы делаем подмену категории ConcreteTargets в скопированном обьекте, чтобы сгенерировать именно то, что нам нужно.
Генерация шаблонов
В этой части, мы будем работать с каждым отдельным шаблоном, для того, чтобы получить сгенерированный код.
Шаблон iBuilder
Этот шаблон нужен для генерации интерфейса IBuilder, который описывает методы, необходимые для всех продуктовых категорий.
package main
type iBuilder interface {
{{range .Properties -}}
set{{if eq .TypeName `int`}}Num{{end}}{{title .Name}}()
{{end -}}
get{{title .ProductTarget}}() {{.ProductTarget}}
}
func getBuilder(builderType string) iBuilder {
{{range .ConcreteTargets -}}
if builderType == "{{.}}" {
return &{{.}}Builder{}
}
{{end -}}
return nil
}
Единственный минус, что перед открытием двух фигурных скобок нужно удалить новую строку и символ пробела.
На строке 4-6 мы проходим циклом и выставляем setter свойство для каждого из них.
На строке 5, мы добавляем необязательное (условное) Num, которое зависит от типа свойства, если оно int
На сроке 11-15 мы проходим категории (Concrete Targets) для создания кода, который будет возвращать iBuilder Interface
Шаблон категории (Concrete target)
Функция processConcreteTargets() вызовет processTemplate() для каждой категории.
В результате, приведенный ниже шаблон будет сгенерирован для каждой конкретной цели, которую мы определили. В наших данных мы определили две конкретные цели — normal и igloo.
package main
{{$target := index .ConcreteTargets 0}}
type {{$target}}Builder struct {
{{range .Properties -}}
{{.Name}} {{.TypeName}}
{{end -}}
}
func new{{title $target}}Builder() *{{$target}}Builder {
return &{{$target}}Builder{}
}
{{range .Properties}}
func (b *{{$target}}Builder) set{{if eq .TypeName `int`}}Num{{end}}{{title .Name}}() {
b.{{.Name}} = *new({{.TypeName}}) // replace this!!
}
{{end}}
func (b *{{$target}}Builder) get{{title .ProductTarget}}() {{.ProductTarget}} {
return {{.ProductTarget}}{
{{range .Properties -}}
{{.Name}}: b.{{.Name}},
{{end -}}
}
}
Здесь обсудим несколько важных моментов:
На строке 3 - создана переменная $target, если вспомните, мы делали подмену списка в нашей функции processConcreteTargets(), вот зачем она. Мы берем 0 элемент массива и хотим быть уверены, что он именно тот что нужен.
На строках 5-7 проходим циклом и инициализируем свойства
В строке 10, мы используем функцию titile из пакета Sprig, он делает заглавной первую букву слова в переменной $target.
В строке 15 мы используем if eq для отображения “Num” перед именем переменной, если тип переменной является целым числом.
В строке 16 ставим комментарий ”// замените это". Этот комментарий является сообщением для разработчика, который будет работать со сгенерированным кодом.
Шаблон продукта (Product)
Этот шаблон генерируется в структуру продукта
package main
type {{.ProductTarget}} struct {
{{range .Properties -}}
{{.Name}} {{.TypeName}}
{{end -}}
}
В строках 4-6 простой цикл, который генерирует все свойства структуры.
Шаблон директора (Director)
package main
type director struct {
builder iBuilder
}
func newDirector(b iBuilder) *director {
return &director{
builder: b,
}
}
func (d *director) setBuilder(b iBuilder) {
d.builder = b
}
func (d *director) build{{title .ProductTarget}}() {{.ProductTarget}} {
{{range .Properties -}}
d.builder.set{{if eq .TypeName `int`}}Num{{end}}{{title .Name}}()
{{end -}}
return d.builder.get{{title .ProductTarget}}()
}
Несколько пояснений:
В строках 17,19 и 21 снова используется функция title из Sprig для заглавной буквы первой строки переменной.
В строках 18-19 мы перебираем свойства по циклу.
В строке 19 мы снова используем условное выражение, чтобы добавить “Num” к свойству, если оно является целым числом.
Небольшой шаблон Main функции
Для того, чтобы разработчик, работающий с нашим сгенерированным кодом мог начать работу с ним, создадим шаблон sample_main.tmpl.
В этом файле есть несколько примеров использования конкретных целевых объектов, созданных с помощью шаблона проектирования Builder.
package main
import "fmt"
func main() {
{{$pt:=.ProductTarget}}
{{$ct:=.ConcreteTargets}}
{{$ps:=.Properties}}
{{range $index, $item := $ct}}
{{$item}}Builder := getBuilder("{{$item}}")
{{if eq $index 0}}director := newDirector({{$item}}Builder) {{else}}director.setBuilder({{$item}}Builder){{end}}
{{$item}}{{title $pt}} := director.build{{title $pt}}()
{{range $p:=$ps -}}
fmt.Printf("{{title $item}} {{title $pt}} {{if eq $p.TypeName `int`}}Num{{end}}{{title $p.Name}}: %v\n", {{$item}}{{title $pt}}.{{$p.Name}})
{{end -}}
{{end}}
}
В строках 6-8 мы создаем множество внутренних переменных для использования в шаблоне.
В строках 9-16 мы перебираем конкретные цели. Обратите внимание, что мы используем $index и $item внутри цикла.
В строке 11 $index используется в условном выражении для определения того, какой оператор должен быть отображен.
В строках 13-15 вложенный цикл используется для печати каждого из свойств конкретных категорий.
Запустим генератор!
Важно, для успешного запуска генератора, нужно создать в корне проекта директорию tmp. Если эта директория не будет создана, то вы получите ошибку:
Writing file: ./tmp/iBuilder.go
panic: open ProductTarget.tmpl: no such file or directory
goroutine 1 [running]:
После успешного запуска, ответ будет следующим
% go run sample_generator.go
Writing file: ./tmp/iBuilder.go
Writing file: ./tmp/house.go
Writing file: ./tmp/director.go
Writing file: ./tmp/sample_main.go
Writing file: ./tmp/normal.go
Writing file: ./tmp/igloo.go
Remember to edit the files that contain the Concrete Targets!
Теперь посмотрим на структуру проекта, а также на то, что сгенерировлось
Ну и при запуске нашего simple_main.go
% go run .
Normal House WindowType:
Normal House DoorType:
Normal House NumFloor: 0
Igloo House WindowType:
Igloo House DoorType:
Igloo House NumFloor: 0
Спасибо за прочтение!