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

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

Avatar for AvitoTech AvitoTech
October 14, 2017

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

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

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

Avatar for AvitoTech

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