Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Кодогенерация в Go — Илья Сауленко (Avito)

AvitoTech
October 14, 2017

Кодогенерация в Go — Илья Сауленко (Avito)

`reflect` медленно работает? `text/template` кидает паники? Скучаешь по дженерикам? Используй кодогенерацию! В докладе речь пойдёт о стандартных средствах Go, помогающих генерировать код, о сторонних библиотеках, ещё больше облегчающих процесс, о плюсах и минусах этого подхода. С примерами и практическим применением, разумеется!

Golang Moscow: Встреча в Avito
14/10/2017
https://golang-moscow.timepad.ru/event/587055/

AvitoTech

October 14, 2017
Tweet

More Decks by AvitoTech

Other Decks in Technology

Transcript

  1. Что такое кодогенерация? Это когда код за тебя пишет компьютер.

    Метапрограммирование, шаблоны кода. CMake C++ Templates LLVM Yacc  mynameiswhm / slides
  2. Проблема Есть сервис хранения метаданных о картинках Метаданные описываются в

    Go-структурах. У структур много вложенных структур Все вложенные структуры - опциональны  mynameiswhm / slides
  3. Пример документа GET /images/42 { "exif": { "camera": { "make":

    "Nikon", "model": "D5" }, "gps": { "coordinates": [55, 80] }, "created": "2017-10-14T16:42:00" }, //... }  mynameiswhm / slides
  4. Клиенты приходят и обновляют часть данных PUT /images/42 { "exif":

    { "gps": { "country": "Russia", "city": "Moscow" } } }  mynameiswhm / slides
  5. Желаемый результат GET /images/42 { "exif": { "camera": { "make":

    "Nikon", "model": "D5" }, "gps": { "coordinates": [55, 80], "country": "Russia", "city": "Moscow" }, "created": "2017-10-14T16:42:00" }, //... }  mynameiswhm / slides
  6. Сервис должен корректно мержить все эти данные type Exif struct

    { Gps *Gps `json:"gps,omitempty"` Camera *Camera `json:"camera,omitempty"` Created *time.Time `json:"created,omitempty"` } type Camera struct { Make *string `json:"make,omitempty"` Model *string `json:"model,omitempty"` } type Gps struct { Coordinates *Coordinates `json:"coordinates,omitempty"` Country *string `json:"country,omitempty"` City *string `json:"gps,omitempty"` } type Coordinates *[2]float32  mynameiswhm / slides
  7. Решение #0 Взять существующую библиотеку: Проблема: никто не любит мержить

    указатели на структуры. github.com/imdario/mergo github.com/divideandconquer/go-merge  mynameiswhm / slides
  8. Решение #0 Указатели на структуры нельзя просто так мержить из-за

    приватных полей: time.Time Реализации hash.Hash  mynameiswhm / slides
  9. Решение #1 Реализуем на структурках метод MergeWith(): func (t *Gps)

    MergeWith(s *Gps) { if s == nil { return } if t == nil { *t = *s return } if s.Coordinates != nil { t.Coordinates = s.Coordinates } if s.Country != nil { t.Country = s.Country } if s.City != nil { t.City = s.City  mynameiswhm / slides
  10. Решение #1 Реализуем на структурках метод MergeWith(): func (t *Exif)

    MergeWith(s *Exif) { if s == nil { return } if t == nil { *t = *s return } if t.Created != nil { s.Created = t.Created } t.Gps.MergeWith(s.Gps) t.Camera.MergeWith(s.Camera) }  mynameiswhm / slides
  11. Решение #1 Плюсы: Compile- me safety Быстро работает Минусы: Много

    кода При расширении структур надо не забыть добавить код в MergeWith()  mynameiswhm / slides
  12. Решение #2 Возьмём reflect и напишем общую реализацию метода Merge():

    func Merge(dst, src interface{}) { // resolve pointer for dst: vDst := reflect.ValueOf(dst).Elem() vSrc := reflect.ValueOf(src) merge(vDst, vSrc) }  mynameiswhm / slides
  13. func merge(dst, src reflect.Value) error { // error handling skipped

    switch dst.Kind() { case reflect.Struct: for i, n := 0, dst.NumField(); i < n; i++ { if err := merge(dst.Field(i), src.Field(i)); err != nil { return err } } case reflect.Ptr: // ... case reflect.Interface: // ... default: if dst.CanSet() { dst.Set(src) } } return nil  mynameiswhm / slides
  14. Решение #2 Решение проблемы с time.Time: У reflect.StructField есть поле

    Tag типа reflect.StructTag. type Exif struct { Gps *Gps `json:"gps,omitempty"` Camera *Camera `json:"camera,omitempty"` Created *time.Time `json:"created,omitempty",merge:"replace"` }  mynameiswhm / slides
  15. Решение #2 Пример из text/template: Работает: Паникует: {{ .non_existent_key.foo }}

    // <no value> {{ (.non_existent_key).foo }} // panic: reflect: Zero(nil) [recovered] golang/go issue#21171  mynameiswhm / slides
  16. Решение #3 Генерировать код метода MergeWith() автоматически из Go- структур.

    Парсить код в AST с помощью go/parser и go/ast, генерировать код как в решении #1.  mynameiswhm / slides
  17. Решение #3 go/parser Метод parser.ParseDir() возвращает map[string]*ast.Package. ast.Package type Package

    struct { Name string // package name Scope *Scope // package scope across all files Imports map[string]*Object // map of package id -> package object Files map[string]*File // Go source files by filename }  mynameiswhm / slides
  18. Решение #3 ast.File type File struct { Doc *CommentGroup //

    associated documentation; or nil Package token.Pos // position of "package" keyword Name *Ident // package name Decls []Decl // top-level declarations; or nil Scope *Scope // package scope (this file only) Imports []*ImportSpec // imports in this file Unresolved []*Ident // unresolved identifiers in this file Comments []*CommentGroup // list of all comments in the source file }  mynameiswhm / slides
  19. Решение #3 ast.Decl ~ ast.Node ast.GenDecl ast.FuncDecl ast.BlockStmt ast.ExprStmt ast.GoStmt

    etc ast.PackageExports() оставляет только экспортированные типы/методы.  mynameiswhm / slides
  20. Решение #3 ast.Walk() / ast.Inspect() для итерации по дереву: func

    Walk(v Visitor, node Node) type Visitor interface { Visit(node Node) (w Visitor) } func Inspect(node Node, f func(Node) bool)  mynameiswhm / slides
  21. Решение #3 type StructsVisitor struct{} func (s *StructsVisitor) Visit(node ast.Node)

    (w ast.Visitor) { switch t := node.(type) { case *ast.TypeSpec: if st, ok := t.Type.(*ast.StructType); ok { if st.Fields == nil { return s } for _, field := range st.Fields.List { typ, ptr := getType(field) tag := getTag(field) for _, name := range field.Names { fmt.Printf("name: %s, type: %s, isPointer: %#v, tag: %s", name } } } }  mynameiswhm / slides
  22. Решение #3 func getType(field *ast.Field) (string, bool) { switch s

    := field.Type.(type) { case *ast.StarExpr: if v, ok := s.X.(*ast.Ident); ok { return v.Name, true } case *ast.Ident: return s.Name, false } return "", false }  mynameiswhm / slides
  23. Решение #3 func getTag(field *ast.Field) string { if field.Tag ==

    nil { return "" } return field.Tag.Value }  mynameiswhm / slides
  24. Решение #3 При желании можно обойтись без Walk() или использовать

    шорткат Scope.Objects: for _, obj := range pkg.Scope.Objects { if t, ok := obj.Decl.(ast.StructType); ok { for _, field := range t.Fields.List { typ, ptr := getType(field) tag := getTag(field) for _, name := range field.Names { fmt.Printf("name: %s, type: %s, isPointer: %#v, tag: %s", name.Nam } } } }  mynameiswhm / slides
  25. Решение #3 Собственно, генерация самого кода: go/ast vs шаблонизация строк

    Генерировать AST — надёжней и предсказуемей Строки шаблонизировать проще  mynameiswhm / slides
  26. Решение #3 go/format — библиотека, предоставляющаяя функционал go fmt. Форматирует

    ноду (ast.File, ast.Decl etc) в io.Writer: Форматирует слайс байтов в слайс байтов: func Node(dst io.Writer, fset *token.FileSet, node interface{}) error func Source(src []byte) ([]byte, error)  mynameiswhm / slides
  27. Решение #3 Ок, код написали, но как его запускать? go

    generate просто делает exec: go generate ./... package main import "fmt" //go:generate echo "123" func main() { fmt.Printf("git version: %s\n", GitVersion) }  mynameiswhm / slides
  28. Решение #3 Пример для генерации version.go вместо ld-флагов: и version_from_git.sh:

    //go:generate version_from_git.sh ##!/bin/bash echo "package main" > version.go echo "const GitVersion = '"$(git rev-parse HEAD)"'" >> version.go  mynameiswhm / slides
  29. Решение #3 Плюсы: Один раз написал — работает Compile- me

    safety Скорость выполнения Можно настраивать поведение Минусы: Много кода для работы с AST  mynameiswhm / slides
  30. Сравнительная табличка Решение Компиляция Скорость Сложность написания Сложность поддержки самописный

    MergeWith() ܔ ܔ ؀ ح reflect ݌ ݌ ب ؀ кодогенерация на go/ast ܔ ܔ ح ؀ кодогенерация на JSON Schema ܔ ܔ ب ؀ Сложность по шкале от ؀ до ح  mynameiswhm / slides
  31. Итоги Юзаем решение #3 с генерацией кода по AST и

    текстовой шаблонизацией. Через теги структур можно настраивать поведение. Планируем выложить утилиту в опенсорс.  mynameiswhm / slides
  32. Бонус: решение #5 Генерировать код на текстовых шаблонах. Можно писать

    на других языках. Хорошо подходит для дженериков: List Queue //go:generate generate-generic -name Exif -type Queue -output exif_list.go  mynameiswhm / slides
  33. Бонус: решение #5 package {{.Package}} type {{.MyType}}Queue struct { q

    []{{.MyType}} } func New{{.MyType}}Queue() *{{.MyType}}Queue { return &{{.MyType}}Queue{ q: []{{.MyType}}{}, } } func (o *{{.MyType}}Queue) Insert(v {{.MyType}}) { o.q = append(o.q, v) } func (o *{{.MyType}}Queue) Remove() {{.MyType}} { if len(o.q) == 0 { panic("Oops.") }  mynameiswhm / slides
  34. Бонус: миграции кода Миграции на уровне AST как в jscodeshi

    . Было: Стало: func foo(a, b, c string) func foo(a, b, c, d string)  mynameiswhm / slides