В данной статье хотелось бы рассмотреть вопросы кодогенерации в Golang. Заметил, что часто в комментариях к статьям по Go упоминают кодогенерацию и рефлексию, что вызывает бурные споры. При этом на хабре статей по кодогенерации мало, хотя она применяется довольно много где в проектах на Go. В статье попытаюсь рассказать, что из себя представляет кодогенерация, описать сферы применения с примерами кода. Также не обойду стороной и рефлексию.
На Хабре есть уже хорошие статьи по теме тут и тут, не буду повторяться.
Кодогенерацию стоит применять в случаях:
Из примеров можно рассмотреть библиотека Stringer, которая входит в стандартную поставку языка и позволяет автоматически генерировать методы String() для наборов числовых констант. С ее помощью можно реализовать вывод имен переменных. Примеры работы библиотеки подробно описали в указанных выше статьях. Наиболее интересный пример был с выводом названия цвета из палитры. Применение кодогенерации там позволяет избежать изменения кода в нескольких местах при изменении палитры.
Из более практического примера можно упомянуть библиотеку easyjson от Mail.ru. Данная библиотека позволяет ускорить выполнение masrshall/unmarshall JSON из/в структуру. Их реализация по бенчмаркам обошла все альтернативные варианты. Для использования библиотеки необходимо вызвать easyjson, он выполнит генерацию кода для всех структур, которые найдет в переданном файле, либо только для тех, к которым указан комментарий //easyjson:json. Возьмем для примера структуру пользователя:
Для файла, в котором она содержится, запустим генерацию кода:
В результате мы получаем для User методы:
Функции MarshalJSON() ([]byte, error) и UnmarshalJSON (data []byte) error необходимы для совместимости со стандартным интерфейсом json.
В данной функции мы сначала преобразуем JSON в структуру, прибавляет один уровень и печатает получившийся JSON. Генерация кода средствами easyjson позволяет избавиться от рефлекции в рантайме и увеличить производительность кода.
Генерация кода активно используется при создании микросервисов, которые взаимодействуют по gRPC. В нем для описания методов сервисов используется формат protobuf — при помощи промежуточного языка EDL. После описания сервиса выполняется запуск компилятора protoc, который генерирует код для нужного языка программирования. В сгененрированном коде мы получаем интерфейсы, которые необходимо реализовать в сервере и методы, которые используются на клиенте для организации общения. Получается довольно удобно, мы можем в едином формате описывать наши сервисы и генерировать код для того языка программирования, на котором будет описывать каждый из элементов взаимодействия.
Также кодогенерация может использоваться при разработке фреймворков. Например, для реализации кода, который не обязательно писать разработчику приложения, но он необходим для корректной работы. Например, для создания валидаторов полей форм, автоматической генерации Middleware, динамической генерации клиентов к СУБД.
Рассмотрим на практике, как же работает механизм кодогенерации в Go. В первую очередь необходимо упомянуть про AST — Abstract Syntax Tree или Абстрактное синтаксическое дерево. За подробностями можно сходить в Википедию. Для наших целей необходимо понимание того, что вся программа строится в виде графа, где вершины сопоставлены (помечены) с операторами языка программирования, а листья — с соответствующими операндами.
Итак, для начала нам понадобятся пакеты:
go/ast
go/parser/
go/token/
Парсинг файла с кодом и составление дерева выполняется следующими командами
Мы указываем, что имя файла взять из первого аргумента командной строки, также просим добавить в дерево комментарии.
В целом для управления кодогенерацией пользователь (разработчик кода, на основании которого генерируется другой код) может использовать комментарии, либо теги (как мы пишем `json:""` возле поля структуры).
Для примера напишем генератор кода для работы с БД. Генератор кода будет просматривать переданный ему файл, искать структуры, у которых есть соответствующий комментарий и создавать обертку над структурой (CRUD методы) для взаимодействия БД. Будем использовать параметры:
Заранее оговорюсь, что проект пока не боевой, в нем не будет части необходимых проверок и защит. Если будет интерес, продолжу его развитие.
Итак, мы определились с задачей и параметрами для управления генерацией кода, можно приступить к написанию кода.
Ссылки на весь код будут в конце статьи.
Начнем обход полученного дерева и будем разбирать каждый элемент первого уровня. В Go для разбора есть предустановленные типы: BadDecl, GenDecl и FuncDecl.
Нас интересуют структуры, поэтому используем GenDecl. На данном этапе может быть полезен FuncDecl, в котором лежат определения функций и вы делаете обертку над ними, но сейчас нам они не нужны. Далее у каждого узла смотрим массив Specs, и ищем, что мы работаем с полем определения типа (*ast.TypeSpec) и это структура (*ast.StructType). После того как мы определили, что перед нами структура, проверим, это у нее есть комментарий //dbe. Полный код обхода дерева и определение, с какой структурой работать, ниже.
Теперь подготовим информацию о полях структуры, чтобы потом на основании полученной информации сгенерировать функции создания таблицы (createTable) и CRUD методы.
Мы проходим по всем полям искомой структуры и начинаем разбор тегов каждого поля. С использованием рефлексии мы получаем интересующий нас тег (ведь на поле могут быть другие теги, например, для json). Выполняем разбор содержимого тега и определяем, является ли поле первичным ключом (если указали более одного первичного ключа, ругнемся об этом и остановим выполнение), есть ли требование к тому, чтобы поле было ненулевым, вообще нужно ли работать с БД для этого поля и определим имя столбца, если оно было переопределено в теге. Также нам нужно определить тип столбца таблицы на основании типа поля структуры. Типов полей конечное множество, будем генерировать только для базовых типов, строки все сведем в тип поля TEXT, хотя в целом можно определение типа столбца добавить в теги, чтобы можно было настраивать более тонко. С другой стороны, никто не мешает создать нужную таблицу в базе заранее, либо скорректировать созданную автоматически.
После разбора структуры запускаем метод для создания кода функции создания таблицы и методы для создания функций Create, Query, Update, Delete. Мы готовим SQL выражение для каждой функции и обвязку для запуска. С обработкой ошибок не стал сильно заморачиваться, просто отдаю ошибку от драйвера БД. Для кодогенерации удобно использовать шаблоны из библиотеки text/template. С их помощью можно получить гораздо более поддерживаемый и предсказуемый код (код виден сразу, а не размазан по коду генератора).
Запуск получившегося кодогенератора выполняется обычным go run, передаем в флаг -name путь к файлу, для которого нужно сгенерировать код. В результате получаем файл с суффиксом _dbe, в котором лежит созданный код. Для тестов создадим методы для следующий структуры:
Для тестирования работы сгенерированного кода создадим объект с произвольными данными, создадим для него таблицу (если таблица существует в базе, вернется ошибка). После поместим этот объект в таблицу, прочитаем все поля из таблицы, обновим значения уровня и удалим объект.
В текущей реализации функционал клиента к БД сильно ограничен:
Однако, исправление недочетов уже за пределами вопроса разбора исходного кода на Go и генерирования на его основе нового кода.
Использование генератора кода в подобном сценарии позволит менять поля и типы структур, используемых в приложении, только в одном месте, нет необходимости помнить о внесении изменений в код взаимодействия с БД, просто каждый раз необходимо запускать генератор кода. Данную задачу можно было решить с помощью рефлексии, но это бы отразилось на производительности.
Исходники генератора кода и пример сгенерированного кода выложил на Github.
Когда применяется кодогенерация
На Хабре есть уже хорошие статьи по теме тут и тут, не буду повторяться.
Кодогенерацию стоит применять в случаях:
- Увеличение скорости работы кода, то есть для замены рефлексии;
- Уменьшение рутинной работы программиста (и ошибок, связанных с ней);
- Реализацию оберток по заданным правилам.
Из примеров можно рассмотреть библиотека Stringer, которая входит в стандартную поставку языка и позволяет автоматически генерировать методы String() для наборов числовых констант. С ее помощью можно реализовать вывод имен переменных. Примеры работы библиотеки подробно описали в указанных выше статьях. Наиболее интересный пример был с выводом названия цвета из палитры. Применение кодогенерации там позволяет избежать изменения кода в нескольких местах при изменении палитры.
Из более практического примера можно упомянуть библиотеку easyjson от Mail.ru. Данная библиотека позволяет ускорить выполнение masrshall/unmarshall JSON из/в структуру. Их реализация по бенчмаркам обошла все альтернативные варианты. Для использования библиотеки необходимо вызвать easyjson, он выполнит генерацию кода для всех структур, которые найдет в переданном файле, либо только для тех, к которым указан комментарий //easyjson:json. Возьмем для примера структуру пользователя:
type User struct{ ID int Login string Email string Level int }
Для файла, в котором она содержится, запустим генерацию кода:
easyjson -all main.go
В результате мы получаем для User методы:
- MarshalEasyJSON (w*jwriter.Writer) — для преобразования структуры в массив байт JSON;
- UnmarshalEasyJSON (l *jlexer.Lexer) — для преобразования из массива байт в структуру.
Функции MarshalJSON() ([]byte, error) и UnmarshalJSON (data []byte) error необходимы для совместимости со стандартным интерфейсом json.
Код работы с easyjson
func TestEasyJSON() { testJSON := `{"ID":123, "Login":"TestUser", "Email":"user@gmail.com", "Level":12}` JSONb := []byte(testJSON) fmt.Println(testJSON) recvUser := &User{} recvUser.UnmarshalJSON(JSONb) fmt.Println(recvUser) recvUser.Level += 1 outJSON, _ := recvUser.MarshalJSON() fmt.Println(string(outJSON)) }
В данной функции мы сначала преобразуем JSON в структуру, прибавляет один уровень и печатает получившийся JSON. Генерация кода средствами easyjson позволяет избавиться от рефлекции в рантайме и увеличить производительность кода.
Генерация кода активно используется при создании микросервисов, которые взаимодействуют по gRPC. В нем для описания методов сервисов используется формат protobuf — при помощи промежуточного языка EDL. После описания сервиса выполняется запуск компилятора protoc, который генерирует код для нужного языка программирования. В сгененрированном коде мы получаем интерфейсы, которые необходимо реализовать в сервере и методы, которые используются на клиенте для организации общения. Получается довольно удобно, мы можем в едином формате описывать наши сервисы и генерировать код для того языка программирования, на котором будет описывать каждый из элементов взаимодействия.
Также кодогенерация может использоваться при разработке фреймворков. Например, для реализации кода, который не обязательно писать разработчику приложения, но он необходим для корректной работы. Например, для создания валидаторов полей форм, автоматической генерации Middleware, динамической генерации клиентов к СУБД.
Реализация кодогенератора на Go
Рассмотрим на практике, как же работает механизм кодогенерации в Go. В первую очередь необходимо упомянуть про AST — Abstract Syntax Tree или Абстрактное синтаксическое дерево. За подробностями можно сходить в Википедию. Для наших целей необходимо понимание того, что вся программа строится в виде графа, где вершины сопоставлены (помечены) с операторами языка программирования, а листья — с соответствующими операндами.
Итак, для начала нам понадобятся пакеты:
go/ast
go/parser/
go/token/
Парсинг файла с кодом и составление дерева выполняется следующими командами
fset := token.NewFileSet() node, err := parser.ParseFile(fset, os.Args[1], nil, parser.ParseComments)
Мы указываем, что имя файла взять из первого аргумента командной строки, также просим добавить в дерево комментарии.
В целом для управления кодогенерацией пользователь (разработчик кода, на основании которого генерируется другой код) может использовать комментарии, либо теги (как мы пишем `json:""` возле поля структуры).
Для примера напишем генератор кода для работы с БД. Генератор кода будет просматривать переданный ему файл, искать структуры, у которых есть соответствующий комментарий и создавать обертку над структурой (CRUD методы) для взаимодействия БД. Будем использовать параметры:
- комментарий dbe:{«table»:«users»}, в котором можно определить таблицу, в которой будут записи структур;
- тег dbe у полей структуры, в котором можно указать имя столбца, в который помещать значение поля и атрибуты для БД: primary_key и not_null. Они будут использоваться при создании таблицы. Причем для имени поля можно использовать "-", чтобы не создавать для него столбец.
Заранее оговорюсь, что проект пока не боевой, в нем не будет части необходимых проверок и защит. Если будет интерес, продолжу его развитие.
Итак, мы определились с задачей и параметрами для управления генерацией кода, можно приступить к написанию кода.
Ссылки на весь код будут в конце статьи.
Начнем обход полученного дерева и будем разбирать каждый элемент первого уровня. В Go для разбора есть предустановленные типы: BadDecl, GenDecl и FuncDecl.
Описание типов
// A BadDecl node is a placeholder for declarations containing // syntax errors for which no correct declaration nodes can be // created. // BadDecl struct { From, To token.Pos // position range of bad declaration } // A GenDecl node (generic declaration node) represents an import, // constant, type or variable declaration. A valid Lparen position // (Lparen.IsValid()) indicates a parenthesized declaration. // // Relationship between Tok value and Specs element type: // // token.IMPORT *ImportSpec // token.CONST *ValueSpec // token.TYPE *TypeSpec // token.VAR *ValueSpec // GenDecl struct { Doc *CommentGroup // associated documentation; or nil TokPos token.Pos // position of Tok Tok token.Token // IMPORT, CONST, TYPE, VAR Lparen token.Pos // position of '(', if any Specs []Spec Rparen token.Pos // position of ')', if any } // A FuncDecl node represents a function declaration. FuncDecl struct { Doc *CommentGroup // associated documentation; or nil Recv *FieldList // receiver (methods); or nil (functions) Name *Ident // function/method name Type *FuncType // function signature: parameters, results, and position of "func" keyword Body *BlockStmt // function body; or nil for external (non-Go) function }
Нас интересуют структуры, поэтому используем GenDecl. На данном этапе может быть полезен FuncDecl, в котором лежат определения функций и вы делаете обертку над ними, но сейчас нам они не нужны. Далее у каждого узла смотрим массив Specs, и ищем, что мы работаем с полем определения типа (*ast.TypeSpec) и это структура (*ast.StructType). После того как мы определили, что перед нами структура, проверим, это у нее есть комментарий //dbe. Полный код обхода дерева и определение, с какой структурой работать, ниже.
Обход дерева и получение структур
Для хранения информации о целевой структуре, создадим структуры:
for _, f := range node.Decls { genD, ok := f.(*ast.GenDecl) if !ok { fmt.Printf("SKIP %T is not *ast.GenDecl\n", f) continue } targetStruct := &StructInfo{} var thisIsStruct bool for _, spec := range genD.Specs { currType, ok := spec.(*ast.TypeSpec) if !ok { fmt.Printf("SKIP %T is not ast.TypeSpec\n", spec) continue } currStruct, ok := currType.Type.(*ast.StructType) if !ok { fmt.Printf("SKIP %T is not ast.StructType\n", currStruct) continue } targetStruct.Name = currType.Name.Name thisIsStruct = true } //Getting comments var needCodegen bool var dbeParams string if thisIsStruct { for _, comment := range genD.Doc.List { needCodegen = needCodegen || strings.HasPrefix(comment.Text, "// dbe") if len(comment.Text) < 7 { dbeParams = "" } else { dbeParams = strings.Replace(comment.Text, "// dbe:", "", 1) } } } if needCodegen { targetStruct.Target = genD genParams := &DbeParam{} if len(dbeParams) != 0 { err := json.Unmarshal([]byte(dbeParams), genParams) if err != nil { fmt.Printf("Error encoding DBE params for structure %s\n", targetStruct.Name) continue } } else { genParams.TableName = targetStruct.Name } targetStruct.GenParam = genParams generateMethods(targetStruct, out) } }
Для хранения информации о целевой структуре, создадим структуры:
type DbeParam struct { TableName string `json:"table"` } type StructInfo struct { Name string GenParam *DbeParam Target *ast.GenDecl }
Теперь подготовим информацию о полях структуры, чтобы потом на основании полученной информации сгенерировать функции создания таблицы (createTable) и CRUD методы.
Код получения полей из структуры
func generateMethods(reqStruct *StructInfo, out *os.File) { for _, spec := range reqStruct.Target.Specs { fmt.Fprintln(out, "") currType, ok := spec.(*ast.TypeSpec) if !ok { continue } currStruct, ok := currType.Type.(*ast.StructType) if !ok { continue } fmt.Printf("\tgenerating createTable methods for %s\n", currType.Name.Name) curTable := &TableInfo{ TableName: reqStruct.GenParam.TableName, Columns: make([]*ColInfo, 0, len(currStruct.Fields.List)), } for _, field := range currStruct.Fields.List { if len(field.Names) == 0 { continue } tableCol := &ColInfo{FieldName: field.Names[0].Name} var fieldIsPrimKey bool var preventThisField bool if field.Tag != nil { tag := reflect.StructTag(field.Tag.Value[1 : len(field.Tag.Value)-1]) tagVal := tag.Get("dbe") fmt.Println("dbe:", tagVal) tagParams := strings.Split(tagVal, ",") PARAMSLOOP: for _, param := range tagParams { switch param { case "primary_key": if curTable.PrimaryKey == nil { fieldIsPrimKey = true tableCol.NotNull = true } else { log.Panicf("Table %s cannot have more then 1 primary key!", currType.Name.Name) } case "not_null": tableCol.NotNull = true case "-": preventThisField = true break PARAMSLOOP default: tableCol.ColName = param } } if preventThisField { continue } } if tableCol.ColName == "" { tableCol.ColName = tableCol.FieldName } if fieldIsPrimKey { curTable.PrimaryKey = tableCol } //Determine field type var fieldType string switch field.Type.(type) { case *ast.Ident: fieldType = field.Type.(*ast.Ident).Name case *ast.SelectorExpr: fieldType = field.Type.(*ast.SelectorExpr).Sel.Name } //fieldType := field.Type.(*ast.Ident).Name fmt.Printf("%s- %s\n", tableCol.FieldName, fieldType) //Check for integers if strings.Contains(fieldType, "int") { tableCol.ColType = "integer" } else { //Check for other types switch fieldType { case "string": tableCol.ColType = "text" case "bool": tableCol.ColType = "boolean" case "Time": tableCol.ColType = "TIMESTAMP" default: log.Panicf("Field type %s not supported", fieldType) } } tableCol.FieldType = fieldType curTable.Columns = append(curTable.Columns, tableCol) curTable.StructName = currType.Name.Name } curTable.generateCreateTable(out) fmt.Printf("\tgenerating CRUD methods for %s\n", currType.Name.Name) curTable.generateCreate(out) curTable.generateQuery(out) curTable.generateUpdate(out) curTable.generateDelete(out) } }
Мы проходим по всем полям искомой структуры и начинаем разбор тегов каждого поля. С использованием рефлексии мы получаем интересующий нас тег (ведь на поле могут быть другие теги, например, для json). Выполняем разбор содержимого тега и определяем, является ли поле первичным ключом (если указали более одного первичного ключа, ругнемся об этом и остановим выполнение), есть ли требование к тому, чтобы поле было ненулевым, вообще нужно ли работать с БД для этого поля и определим имя столбца, если оно было переопределено в теге. Также нам нужно определить тип столбца таблицы на основании типа поля структуры. Типов полей конечное множество, будем генерировать только для базовых типов, строки все сведем в тип поля TEXT, хотя в целом можно определение типа столбца добавить в теги, чтобы можно было настраивать более тонко. С другой стороны, никто не мешает создать нужную таблицу в базе заранее, либо скорректировать созданную автоматически.
После разбора структуры запускаем метод для создания кода функции создания таблицы и методы для создания функций Create, Query, Update, Delete. Мы готовим SQL выражение для каждой функции и обвязку для запуска. С обработкой ошибок не стал сильно заморачиваться, просто отдаю ошибку от драйвера БД. Для кодогенерации удобно использовать шаблоны из библиотеки text/template. С их помощью можно получить гораздо более поддерживаемый и предсказуемый код (код виден сразу, а не размазан по коду генератора).
Создание таблицы
func (tableD *TableInfo) generateCreateTable(out *os.File) error { fmt.Fprint(out, "func (in *"+tableD.StructName+") createTable(db *sql.DB) (error) {\n") var resSQLq = fmt.Sprintf("\tsqlQ := `CREATE TABLE %s (\n", tableD.TableName) for _, col := range tableD.Columns { colSQL := col.ColName + " " + col.ColType if col.NotNull { colSQL += " NOT NULL" } if col == tableD.PrimaryKey { colSQL += " AUTO_INCREMENT" } colSQL += ",\n" resSQLq += colSQL } if tableD.PrimaryKey != nil { resSQLq += fmt.Sprintf("PRIMARY KEY (%s)\n", tableD.PrimaryKey.ColName) } resSQLq += ")`\n" fmt.Fprint(out, resSQLq) fmt.Fprint(out, "\t_, err := db.Exec(sqlQ)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n") fmt.Fprint(out, "\t return nil\n}\n\n") return nil }
Добавление записи
fmt.Fprint(out, "func (in *"+tableD.StructName+") Create(db *sql.DB) (error) {\n") var columns, valuePlaces, valuesListParams string for _, col := range tableD.Columns { if col == tableD.PrimaryKey { continue } columns += "`" + col.ColName + "`," valuePlaces += "?," valuesListParams += "in." + col.FieldName + "," } columns = columns[:len(columns)-1] valuePlaces = valuePlaces[:len(valuePlaces)-1] valuesListParams = valuesListParams[:len(valuesListParams)-1] resSQLq := fmt.Sprintf("\tsqlQ := \"INSERT INTO %s (%s) VALUES (%s);\"\n", tableD.TableName, columns, valuePlaces) fmt.Fprintln(out, resSQLq) fmt.Fprintf(out, "result, err := db.Exec(sqlQ, %s)\n", valuesListParams) fmt.Fprintln(out, `if err != nil { return err }`) //Setting id if we have primary key if tableD.PrimaryKey != nil { fmt.Fprintf(out, `lastId, err := result.LastInsertId() if err != nil { return nil }`) fmt.Fprintf(out, "\nin.%s = %s(lastId)\n", tableD.PrimaryKey.FieldName, tableD.PrimaryKey.FieldType) } fmt.Fprintln(out, "return nil\n}\n\n") //in., _ := result.LastInsertId()`) return nil }
Получение записей из таблицы
func (tableD *TableInfo) generateQuery(out *os.File) error { fmt.Fprint(out, "func (in *"+tableD.StructName+") Query(db *sql.DB) ([]*"+tableD.StructName+", error) {\n") fmt.Fprintf(out, "\tsqlQ := \"SELECT * FROM %s;\"\n", tableD.TableName) fmt.Fprintf(out, "rows, err := db.Query(sqlQ)\n") fmt.Fprintf(out, "results := make([]*%s, 0)\n", tableD.StructName) fmt.Fprintf(out, `for rows.Next() {`) fmt.Fprintf(out, "\t tempR := &%s{}\n", tableD.StructName) var valuesListParams string for _, col := range tableD.Columns { valuesListParams += "&tempR." + col.FieldName + "," } valuesListParams = valuesListParams[:len(valuesListParams)-1] fmt.Fprintf(out, "\terr = rows.Scan(%s)\n", valuesListParams) fmt.Fprintf(out, `if err != nil { return nil, err }`) fmt.Fprintf(out, "\n\tresults = append(results, tempR)") fmt.Fprintf(out, `} return results, nil }`) fmt.Fprintln(out, "") fmt.Fprintln(out, "") return nil }
Обновление записи (работает по primary key)
func (tableD *TableInfo) generateUpdate(out *os.File) error { fmt.Fprint(out, "func (in *"+tableD.StructName+") Update(db *sql.DB) (error) {\n") var updVals, valuesListParams string for _, col := range tableD.Columns { if col == tableD.PrimaryKey { continue } updVals += "`" + col.ColName + "`=?," valuesListParams += "in." + col.FieldName + "," } updVals = updVals[:len(updVals)-1] valuesListParams += "in." + tableD.PrimaryKey.FieldName resSQLq := fmt.Sprintf("\tsqlQ := \"UPDATE %s SET %s WHERE %s = ?;\"\n", tableD.TableName, updVals, tableD.PrimaryKey.ColName) fmt.Fprintln(out, resSQLq) fmt.Fprintf(out, "_, err := db.Exec(sqlQ, %s)\n", valuesListParams) fmt.Fprintln(out, `if err != nil { return err }`) fmt.Fprintln(out, "return nil\n}\n\n") //in., _ := result.LastInsertId()`) return nil }
Удаление записи (работает по primary key)
func (tableD *TableInfo) generateDelete(out *os.File) error { fmt.Fprint(out, "func (in *"+tableD.StructName+") Delete(db *sql.DB) (error) {\n") fmt.Fprintf(out, "sqlQ := \"DELETE FROM %s WHERE id = ?\"\n", tableD.TableName) fmt.Fprintf(out, "_, err := db.Exec(sqlQ, in.%s)\n", tableD.PrimaryKey.FieldName) fmt.Fprintln(out, `if err != nil { return err } return nil }`) fmt.Fprintln(out) return nil }
Запуск получившегося кодогенератора выполняется обычным go run, передаем в флаг -name путь к файлу, для которого нужно сгенерировать код. В результате получаем файл с суффиксом _dbe, в котором лежит созданный код. Для тестов создадим методы для следующий структуры:
// dbe:{"table": "users"} type User struct { ID int `dbe:"id,primary_key"` Login string `dbe:"login,not_null"` Email string Level uint8 IsActive bool UError error `dbe:"-"` }
Получившийся на выходе код
package main import "database/sql" func (in *User) createTable(db *sql.DB) error { sqlQ := `CREATE TABLE users ( id integer NOT NULL AUTO_INCREMENT, login text NOT NULL, Email text, Level integer, IsActive boolean, PRIMARY KEY (id) )` _, err := db.Exec(sqlQ) if err != nil { return err } return nil } func (in *User) Create(db *sql.DB) error { sqlQ := "INSERT INTO users (`login`,`Email`,`Level`,`IsActive`) VALUES (?,?,?,?);" result, err := db.Exec(sqlQ, in.Login, in.Email, in.Level, in.IsActive) if err != nil { return err } lastId, err := result.LastInsertId() if err != nil { return nil } in.ID = int(lastId) return nil } func (in *User) Query(db *sql.DB) ([]*User, error) { sqlQ := "SELECT * FROM users;" rows, err := db.Query(sqlQ) results := make([]*User, 0) for rows.Next() { tempR := &User{} err = rows.Scan(&tempR.ID, &tempR.Login, &tempR.Email, &tempR.Level, &tempR.IsActive) if err != nil { return nil, err } results = append(results, tempR) } return results, nil } func (in *User) Update(db *sql.DB) error { sqlQ := "UPDATE users SET `login`=?,`Email`=?,`Level`=?,`IsActive`=? WHERE id = ?;" _, err := db.Exec(sqlQ, in.Login, in.Email, in.Level, in.IsActive, in.ID) if err != nil { return err } return nil } func (in *User) Delete(db *sql.DB) error { sqlQ := "DELETE FROM users WHERE id = ?" _, err := db.Exec(sqlQ, in.ID) if err != nil { return err } return nil }
Для тестирования работы сгенерированного кода создадим объект с произвольными данными, создадим для него таблицу (если таблица существует в базе, вернется ошибка). После поместим этот объект в таблицу, прочитаем все поля из таблицы, обновим значения уровня и удалим объект.
Вызов получившихся методов
var err error db, err := sql.Open("mysql", DSN) if err != nil { fmt.Println("Unable to connect to DB", err) return } err = db.Ping() if err != nil { fmt.Println("Unable to ping BD") return } newUser := &User{ Login: "newUser", Email: "new@test.com", Level: 0, IsActive: false, UError: nil, } err = newUser.createTable(db) if err != nil { fmt.Println("Error creating table.", err) } err = newUser.Create(db) if err != nil { fmt.Println("Error creating user.", err) return } nU := &User{} dbUsers, err := nU.Query(db) if err != nil { fmt.Println("Error selecting users.", err) return } fmt.Printf("From table users selected %d fields", len(dbUsers)) var DBUser *User for _, user := range dbUsers { fmt.Println(user) DBUser = user } DBUser.Level = 2 err = DBUser.Update(db) if err != nil { fmt.Println("Error updating users.", err) return } err = DBUser.Delete(db) if err != nil { fmt.Println("Error deleting users.", err) return }
В текущей реализации функционал клиента к БД сильно ограничен:
- поддерживается только MySQL;
- не все типы полей поддерживаются;
- для SELECT нет фильтрации и лимитов.
Однако, исправление недочетов уже за пределами вопроса разбора исходного кода на Go и генерирования на его основе нового кода.
Использование генератора кода в подобном сценарии позволит менять поля и типы структур, используемых в приложении, только в одном месте, нет необходимости помнить о внесении изменений в код взаимодействия с БД, просто каждый раз необходимо запускать генератор кода. Данную задачу можно было решить с помощью рефлексии, но это бы отразилось на производительности.
Исходники генератора кода и пример сгенерированного кода выложил на Github.
