Codegenerierung in Go am Beispiel des Erstellens eines Clients für die Datenbank

In diesem Artikel möchte ich die Probleme der Codegenerierung in Golang betrachten. Mir ist aufgefallen, dass in den Kommentaren zu Artikeln auf Go häufig die Generierung und Reflexion von Code erwähnt wird, was zu heftigen Debatten führt. Gleichzeitig gibt es auf dem Hub nur wenige Artikel zur Codegenerierung, die jedoch häufig in Go-Projekten verwendet werden. In dem Artikel werde ich versuchen, Ihnen zu erklären, was Codegenerierung ist, um den Anwendungsbereich anhand von Codebeispielen zu beschreiben. Auch werde ich die Reflexion nicht ignorieren.

Wenn die Codegenerierung verwendet wird


Auf Habré gibt es hier und hier bereits gute Artikel zum Thema , die ich nicht wiederholen werde.

Die Codegenerierung sollte in folgenden Fällen verwendet werden:

  • Erhöhen der Geschwindigkeit des Codes, dh Ersetzen der Reflexion;
  • Reduzierung der Routine eines Programmierers (und der damit verbundenen Fehler);
  • Implementierung von Wrappern nach den vorgegebenen Regeln.

Anhand der Beispiele können wir die Stringer-Bibliothek betrachten, die im Standard-Sprachangebot enthalten ist und es Ihnen ermöglicht, automatisch String () -Methoden für Sätze numerischer Konstanten zu generieren. Mit ihm können Sie die Ausgabe von Variablennamen implementieren. Beispiele der Bibliothek wurden in den obigen Artikeln ausführlich beschrieben. Das interessanteste Beispiel war die Ableitung des Farbnamens aus der Palette. Durch die Anwendung der Codegenerierung wird vermieden, dass der Code beim Ändern der Palette an mehreren Stellen geändert wird.

Anhand eines praktischeren Beispiels können wir die easyjson-Bibliothek von Mail.ru erwähnen. Mit dieser Bibliothek können Sie die Ausführung von masrshall / unmarshall JSON von / zur Struktur beschleunigen. Ihre Implementierung in Benchmarks umging alle Alternativen. Um die Bibliothek zu verwenden, müssen Sie easyjson aufrufen. Sie generiert Code für alle Strukturen, die in der übertragenen Datei gefunden werden, oder nur für diejenigen, für die der Kommentar // easyjson: json angegeben ist. Nehmen Sie die Benutzerstruktur als Beispiel:

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

Führen Sie für die Datei, in der sie enthalten ist, die Codegenerierung aus:

easyjson -all main.go

Als Ergebnis erhalten wir Methoden für Benutzer:

  • MarshalEasyJSON (w * jwriter.Writer) - um die Struktur in ein JSON-Byte-Array zu konvertieren;
  • UnmarshalEasyJSON (l * jlexer.Lexer) - zum Konvertieren von einem Array von Bytes in eine Struktur.

Die Funktionen MarshalJSON () ([] Byte, Fehler) und UnmarshalJSON (Daten [] Byte) Fehler sind für die Kompatibilität mit der Standard-JSON-Schnittstelle erforderlich.

Easyjson-Code

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


In dieser Funktion konvertieren wir zuerst JSON in eine Struktur, fügen eine Ebene hinzu und drucken den resultierenden JSON. Die Codegenerierung durch easyjson bedeutet, die Laufzeitreflexion zu beseitigen und die Codeleistung zu steigern.

Die Codegenerierung wird aktiv verwendet, um Microservices zu erstellen, die über gRPC kommunizieren. Es verwendet das Protobuf-Format, um die Methoden der Dienste zu beschreiben - unter Verwendung der Zwischensprache EDL. Nach der Beschreibung des Dienstes wird der Protoc-Compiler gestartet, der Code für die gewünschte Programmiersprache generiert. Im generierten Code erhalten wir die Schnittstellen, die auf dem Server implementiert werden müssen, und die Methoden, die auf dem Client zum Organisieren der Kommunikation verwendet werden. Es stellt sich als sehr praktisch heraus, dass wir unsere Dienste in einem einzigen Format beschreiben und Code für die Programmiersprache generieren können, in der jedes der Interaktionselemente beschrieben wird.

Die Codegenerierung kann auch bei der Entwicklung von Frameworks verwendet werden. Zum Beispiel, um Code zu implementieren, der nicht vom Anwendungsentwickler geschrieben werden muss, aber für den korrekten Betrieb erforderlich ist. Zum Beispiel, um Formularfeldvalidatoren, automatische Middleware-Generierung und dynamische Generierung von Clients für das DBMS zu erstellen.

Go Code Generator Implementierung


Lassen Sie uns in der Praxis untersuchen, wie der Mechanismus der Codegenerierung in Go funktioniert. Zunächst ist der AST - Abstract Syntax Tree oder Abstract Syntax Tree zu erwähnen. Für Details können Sie zu Wikipedia gehen . Für unsere Zwecke ist es notwendig zu verstehen, dass das gesamte Programm in Form eines Diagramms erstellt wird, in dem die Scheitelpunkte mit den Operatoren der Programmiersprache und die Blätter mit den entsprechenden Operanden abgebildet (markiert) werden.

Für den Anfang benötigen wir also Pakete:

go / ast
go / parser /
go / token / Das

Parsen der Datei mit dem Code und das Kompilieren des Baums erfolgt mit den folgenden Befehlen


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

Wir geben an, dass der Dateiname dem ersten Argument der Befehlszeile entnommen werden soll. Wir bitten außerdem, dem Baum Kommentare hinzuzufügen.

Im Allgemeinen kann der Benutzer (der Entwickler des Codes, auf dessen Grundlage ein anderer Code generiert wird) zur Steuerung der Codegenerierung Kommentare oder Tags verwenden (während wir "json:" "in der Nähe des Strukturfelds schreiben).

Als Beispiel schreiben wir einen Codegenerator für die Arbeit mit einer Datenbank. Der Codegenerator überprüft die in ihn übertragene Datei, sucht nach Strukturen mit einem entsprechenden Kommentar und erstellt einen Wrapper über der Struktur (CRUD-Methoden) für die Datenbankinteraktion. Wir werden die Parameter verwenden:

  • dbe comment: {"table": "users"}, in dem Sie die Tabelle definieren können, in der sich die Strukturdatensätze befinden sollen;
  • dbe-Tag für die Felder der Struktur, in dem Sie den Namen der Spalte angeben können, in die der Feldwert und die Attribute für die Datenbank eingefügt werden sollen: primary_key und not_null. Sie werden beim Erstellen der Tabelle verwendet. Und für den Feldnamen können Sie "-" verwenden, um keine Spalte dafür zu erstellen.

Ich werde im Voraus reservieren, dass das Projekt noch nicht im Kampf ist, es wird keinen Teil der notwendigen Kontrollen und Schutzmaßnahmen enthalten. Bei Interesse werde ich mich weiterentwickeln.

Nachdem wir uns für die Aufgabe und die Parameter zur Steuerung der Codegenerierung entschieden haben, können wir mit dem Schreiben von Code beginnen.

Links zu allen Codes finden Sie am Ende des Artikels.

Wir umgehen den resultierenden Baum und analysieren jedes Element der ersten Ebene. Go verfügt über vordefinierte Parsing-Typen: BadDecl, GenDecl und FuncDecl.

Typ Beschreibung
// 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
}


Wir sind an Strukturen interessiert, deshalb verwenden wir GenDecl. In diesem Stadium kann FuncDecl nützlich sein, in dem die Definitionen von Funktionen liegen und Sie sie umschließen, aber jetzt brauchen wir sie nicht. Als nächstes betrachten wir das Specs-Array an jedem Knoten und suchen, ob wir mit einem Typdefinitionsfeld (* ast.TypeSpec) arbeiten. Dies ist eine Struktur (* ast.StructType). Nachdem wir festgestellt haben, dass wir eine Struktur haben, überprüfen wir, ob sie einen Kommentar // dbe enthält. Der vollständige Baumdurchlaufcode und die Definition, mit welcher Struktur gearbeitet werden soll, sind unten aufgeführt.

Baum durchqueren und Strukturen bekommen

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
}


Jetzt bereiten wir Informationen über die Felder der Struktur vor, sodass wir basierend auf den empfangenen Informationen Tabellenerstellungsfunktionen (createTable) und CRUD-Methoden generieren.

Code zum Abrufen von Feldern aus einer Struktur

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


Wir gehen alle Felder der gewünschten Struktur durch und beginnen, die Tags jedes Feldes zu analysieren. Mit Reflection erhalten wir das Tag, an dem wir interessiert sind (schließlich befinden sich möglicherweise andere Tags auf dem Feld, z. B. für json). Wir analysieren den Inhalt des Tags und stellen fest, ob das Feld ein Primärschlüssel ist (wenn mehr als ein Primärschlüssel angegeben ist, verfluchen Sie ihn und stoppen Sie die Ausführung). Muss das Feld ungleich Null sein, müssen wir mit der Datenbank für dieses Feld arbeiten und definieren Spaltenname, wenn er im Tag überschrieben wurde. Wir müssen auch den Typ der Tabellenspalte basierend auf dem Typ des Strukturfelds bestimmen. Es gibt eine endliche Menge von Feldtypen, die nur für Basistypen generiert werden. Wir reduzieren alle Zeilen auf den TEXT-Feldtyp. Im Allgemeinen können Sie den Tags jedoch eine Spaltentypdefinition hinzufügen, damit Sie sie feiner anpassen können. Andererseits,niemand stört sich daran, die gewünschte Tabelle im Voraus in der Datenbank zu erstellen oder die automatisch erstellte zu korrigieren.

Nach dem Parsen der Struktur starten wir die Methode zum Erstellen des Codes für die Tabellenerstellungsfunktion und die Methoden zum Erstellen der Funktionen Erstellen, Abfragen, Aktualisieren und Löschen. Wir bereiten für jede Funktion einen SQL-Ausdruck und eine auszuführende Bindung vor. Ich habe mich nicht um die Fehlerbehandlung gekümmert, sondern nur den Fehler vom Datenbanktreiber angegeben. Für die Codegenerierung ist es bequem, Vorlagen aus der Text- / Vorlagenbibliothek zu verwenden. Mit ihrer Hilfe können Sie einen viel besser unterstützten und vorhersehbaren Code erhalten (der Code ist sofort sichtbar, aber nicht durch den Generatorcode verschmiert).

Tabellenerstellung

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
}


Datensatz hinzufügen


	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
}


Datensätze aus einer Tabelle abrufen

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
}


Update aufzeichnen (funktioniert nach Primärschlüssel)

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
}


Datensatz löschen (funktioniert nach Primärschlüssel)

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
}


Der Start des resultierenden Codegenerators erfolgt durch den üblichen Start. Wir übergeben den Pfad zu der Datei, für die Sie den Code generieren möchten, im Flag -name. Als Ergebnis erhalten wir die Datei mit dem Suffix _dbe, in dem der generierte Code liegt. Erstellen Sie für Tests Methoden für die folgende Struktur:


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

Der resultierende Code

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
}


Um die Funktionsweise des generierten Codes zu testen, erstellen Sie ein Objekt mit beliebigen Daten und erstellen Sie eine Tabelle dafür (wenn die Tabelle in der Datenbank vorhanden ist, wird ein Fehler zurückgegeben). Nachdem wir dieses Objekt in die Tabelle eingefügt haben, lesen Sie alle Felder aus der Tabelle, aktualisieren Sie die Ebenenwerte und löschen Sie das Objekt.

Rufen Sie die resultierenden Methoden auf

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
}


In der aktuellen Implementierung ist die Funktionalität des Clients für die Datenbank sehr eingeschränkt:

  • Es wird nur MySQL unterstützt.
  • Nicht alle Feldtypen werden unterstützt.
  • Es gibt keine Filterung und Grenzen für SELECT.

Das Beheben von Fehlern geht jedoch bereits über das Analysieren von Go-Quellcode und das Generieren von neuem Code auf dieser Grundlage hinaus.

Wenn Sie in einem solchen Szenario einen Codegenerator verwenden, können Sie die in der Anwendung verwendeten Felder und Strukturtypen nur an einer Stelle ändern. Sie müssen nicht daran denken, Änderungen am Code für die Interaktion mit der Datenbank vorzunehmen. Sie müssen lediglich den Codegenerator jedes Mal ausführen. Diese Aufgabe könnte mit Hilfe der Reflexion gelöst werden, dies hätte jedoch die Leistung beeinträchtigt.

Der Quellcode-Generator und ein Beispiel für den generierten Code, der auf Github veröffentlicht wurde .

All Articles