Génération de code dans Go par l'exemple de la création d'un client pour la base de données

Dans cet article, je voudrais examiner les problèmes de génération de code dans Golang. J'ai remarqué que souvent dans les commentaires sur les articles sur Go mentionnent la génération et la réflexion de code, ce qui provoque un débat houleux. Dans le même temps, il y a peu d'articles sur la génération de code sur le hub, bien qu'il soit beaucoup utilisé dans les projets sur Go. Dans l'article, je vais essayer de vous dire ce qu'est la génération de code, pour décrire le champ d'application avec des exemples de code. Aussi, je n'ignorerai pas le reflet.

Lorsque la génération de code est utilisée


Sur Habré il y a déjà de bons articles sur le sujet ici et ici , je ne vais pas répéter.

La génération de code doit être utilisée dans les cas:

  • Augmenter la vitesse du code, c'est-Ă -dire remplacer la rĂ©flexion;
  • RĂ©duire la routine d'un programmeur (et les erreurs qui lui sont associĂ©es);
  • ImplĂ©mentation de wrappers selon les règles donnĂ©es.

À partir des exemples, nous pouvons considérer la bibliothèque Stringer, qui est incluse dans la fourniture de langage standard et vous permet de générer automatiquement des méthodes String () pour des ensembles de constantes numériques. En l'utilisant, vous pouvez implémenter la sortie des noms de variables. Des exemples de la bibliothèque ont été décrits en détail dans les articles ci-dessus. L'exemple le plus intéressant était la dérivation du nom de la couleur de la palette. L'application de la génération de code y évite de changer le code à plusieurs endroits lors du changement de palette.

À partir d'un exemple plus pratique, nous pouvons mentionner la bibliothèque easyjson de Mail.ru. Cette bibliothèque vous permet d'accélérer l'exécution de masrshall / unmarshall JSON de / vers la structure. Leur implémentation sur des benchmarks a contourné toutes les alternatives. Pour utiliser la bibliothèque, vous devez appeler easyjson, elle générera du code pour toutes les structures qu'elle trouve dans le fichier transféré, ou seulement pour celles qui sont commentées // easyjson: json. Prenons l'exemple de la structure des utilisateurs:

type User struct{
    ID int
    Login string
    Email string
    Level int
}

Pour le fichier dans lequel il se trouve, exécutez la génération de code:

easyjson -all main.go

En conséquence, nous obtenons des méthodes pour l'utilisateur:

  • MarshalEasyJSON (w * jwriter.Writer) - pour convertir la structure en un tableau d'octets JSON;
  • UnmarshalEasyJSON (l * jlexer.Lexer) - pour convertir d'un tableau d'octets en une structure.

Les fonctions MarshalJSON () ([] octet, erreur) et UnmarshalJSON (données [] octet) sont nécessaires pour la compatibilité avec l'interface json standard.

Code 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))
}


Dans cette fonction, nous convertissons d'abord le JSON en une structure, ajoutons un niveau et imprimons le JSON résultant. La génération de code par easyjson signifie se débarrasser de la réflexion d'exécution et augmenter les performances du code.

La génération de code est activement utilisée pour créer des microservices qui communiquent via gRPC. Il utilise le format protobuf pour décrire les méthodes de services - en utilisant le langage intermédiaire EDL. Après la description du service, le compilateur de protocole est lancé, ce qui génère du code pour le langage de programmation souhaité. Dans le code généré, nous obtenons les interfaces qui doivent être implémentées dans le serveur et les méthodes utilisées sur le client pour organiser la communication. Il s'avère assez pratique, nous pouvons décrire nos services dans un format unique et générer du code pour le langage de programmation dans lequel chacun des éléments d'interaction sera décrit.

De plus, la génération de code peut être utilisée dans le développement de frameworks. Par exemple, pour implémenter du code qui ne doit pas être écrit par le développeur de l'application, mais qui est nécessaire pour un fonctionnement correct. Par exemple, pour créer des validateurs de champs de formulaire, la génération automatique de middleware, la génération dynamique de clients vers le SGBD.

Mise en œuvre du générateur de code Go


Examinons en pratique le fonctionnement du mécanisme de génération de code dans Go. Tout d'abord, il est nécessaire de mentionner l'AST - Abstract Syntax Tree ou Abstract Syntax Tree. Pour plus de détails, vous pouvez aller sur Wikipedia . Pour nos besoins, il est nécessaire de comprendre que l'ensemble du programme est construit sous la forme d'un graphique, où les sommets sont mappés (marqués) avec les opérateurs du langage de programmation, et les feuilles avec les opérandes correspondants.

Donc, pour commencer, nous avons besoin de packages:

go / ast
go / parser /
go / token / L'

analyse du fichier avec le code et la compilation de l'arborescence sont effectuées par les commandes suivantes


fset := token.NewFileSet()
node, err := parser.ParseFile(fset, os.Args[1], nil, parser.ParseComments)

Nous indiquons que le nom du fichier doit être tiré du premier argument de la ligne de commande, nous demandons également que des commentaires soient ajoutés à l'arborescence.

En général, pour contrôler la génération de code, l'utilisateur (le développeur du code sur la base duquel un autre code est généré) peut utiliser des commentaires ou des balises (comme nous écrivons `json:" "` près du champ de structure).

Par exemple, nous allons écrire un générateur de code pour travailler avec une base de données. Le générateur de code examinera le fichier qui lui est transféré, recherchera les structures qui ont un commentaire correspondant et créera un wrapper sur la structure (méthodes CRUD) pour l'interaction de la base de données. Nous utiliserons les paramètres:

  • dbe comment: {"table": "users"}, dans laquelle vous pouvez dĂ©finir la table dans laquelle les enregistrements de structure seront;
  • Balise dbe pour les champs de la structure, dans laquelle vous pouvez spĂ©cifier le nom de la colonne dans laquelle placer la valeur de champ et les attributs de la base de donnĂ©es: clĂ©_principale et non_null. Ils seront utilisĂ©s lors de la crĂ©ation de la table. Et pour le nom du champ, vous pouvez utiliser "-" afin de ne pas lui crĂ©er de colonne.

Je réserverai à l'avance que le projet n'est pas encore en combat, il ne contiendra pas une partie des contrôles et protections nécessaires. S'il y a un intérêt, je poursuivrai son développement.

Donc, nous avons décidé de la tâche et des paramètres pour contrôler la génération de code, nous pouvons commencer à écrire du code.

Des liens vers tout le code seront Ă  la fin de l'article.

Nous commençons à contourner l'arbre résultant et analyserons chaque élément du premier niveau. Go a des types prédéfinis pour l'analyse: BadDecl, GenDecl et FuncDecl.

Type Description
// 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
}


Nous sommes intéressés par les structures, nous utilisons donc GenDecl. À ce stade, FuncDecl peut être utile, dans lequel se trouvent les définitions des fonctions et vous les encapsulez, mais maintenant nous n'en avons plus besoin. Ensuite, nous regardons le tableau Specs à chaque nœud et cherchons que nous travaillons avec un champ de définition de type (* ast.TypeSpec) et ceci est une structure (* ast.StructType). Après avoir déterminé que nous avons une structure, nous vérifions qu'elle a un commentaire // dbe. Le code de traversée de l'arborescence complète et la définition de la structure avec laquelle travailler sont ci-dessous.

Traversée d'arbres et obtention de structures

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
}


Nous allons maintenant préparer des informations sur les champs de la structure, afin que, sur la base des informations reçues, nous générions des fonctions de création de table (createTable) et des méthodes CRUD.

Code pour obtenir des champs d'une structure

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)
	}
}


Nous parcourons tous les champs de la structure souhaitée et commençons à analyser les balises de chaque champ. En utilisant la réflexion, nous obtenons la balise qui nous intéresse (après tout, il peut y avoir d'autres balises sur le terrain, par exemple, pour json). Nous analysons le contenu de la balise et déterminons si le champ est une clé primaire (si plusieurs clés primaires sont spécifiées, maudissons à ce sujet et arrêtons l'exécution), le champ doit-il être différent de zéro, devons-nous travailler avec la base de données pour ce champ et définir nom de la colonne s'il a été remplacé dans la balise. Nous devons également déterminer le type de la colonne de table en fonction du type du champ de structure. Il existe un ensemble fini de types de champs, nous générerons uniquement pour les types de base, nous réduirons toutes les lignes au type de champ TEXTE, bien qu'en général, vous pouvez ajouter la définition du type de colonne aux balises afin de pouvoir configurer plus finement. D'autre part,personne ne se soucie de créer à l'avance la table souhaitée dans la base de données ou de corriger automatiquement la création.

Après avoir analysé la structure, nous démarrons la méthode de création du code pour la fonction de création de table et les méthodes de création des fonctions Créer, Interroger, Mettre à jour, Supprimer. Nous préparons une expression SQL pour chaque fonction et une liaison à exécuter. Je n'ai pas pris la peine de gérer les erreurs, je donne simplement l'erreur à partir du pilote de base de données. Pour la génération de code, il est pratique d'utiliser des modèles à partir de la bibliothèque de texte / modèle. Avec leur aide, vous pouvez obtenir un code beaucoup plus pris en charge et prévisible (le code est visible immédiatement, mais pas taché par le code du générateur).

Création de table

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
}


Ajouter un enregistrement


	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
}


Récupération des enregistrements d'une table

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
}


Mise à jour des enregistrements (fonctionne par clé primaire)

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
}


Supprimer un enregistrement (fonctionne par clé primaire)

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
}


Le démarrage du générateur de code résultant est effectué par l'exécution habituelle, nous passons le chemin d'accès au fichier pour lequel vous souhaitez générer le code dans l'indicateur -name. En conséquence, nous obtenons le fichier avec le suffixe _dbe, dans lequel se trouve le code généré. Pour les tests, créez des méthodes pour la structure suivante:


// 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:"-"`
}

Le code résultant

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
}


Pour tester le fonctionnement du code généré, créez un objet avec des données arbitraires, créez une table pour celui-ci (si la table existe dans la base de données, une erreur sera retournée). Après avoir placé cet objet dans la table, lisez tous les champs de la table, mettez à jour les valeurs de niveau et supprimez l'objet.

Appelez les méthodes résultantes

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
}


Dans l'implémentation actuelle, la fonctionnalité du client à la base de données est très limitée:

  • seul MySQL est pris en charge;
  • Tous les types de champs ne sont pas pris en charge.
  • il n'y a pas de filtrage et de limites pour SELECT.

Cependant, la correction de bogues dépasse déjà le cadre de l'analyse du code source de Go et la génération de nouveau code à partir de celui-ci.

L'utilisation d'un générateur de code dans un tel scénario vous permettra de modifier les champs et les types de structures utilisés dans l'application en un seul endroit, il n'est pas nécessaire de se rappeler de modifier le code pour interagir avec la base de données, il vous suffit d'exécuter le générateur de code à chaque fois. Cette tâche aurait pu être résolue à l'aide de la réflexion, mais cela aurait affecté les performances.

Le générateur de code source et un exemple du code généré posté sur Github .

All Articles