Vor einiger Zeit habe ich die Programmiersprache Golang und all ihren Nutzen für die Entwickler vorgestellt. Zugegeben, eine in der Konkurrenz sehr verbreitete Funktionalität besitzt Go nicht: Generics. Und diese Funktionalität is noch dazu sehr gefragt. Allein das Gesamtbild der Reaktionen (Smileys) auf den Vorschlag, Generics in Go v2 zu integrieren, sagt mehr als 1000 Worte. Aber es geht auch anders…
Problem
Aktuell arbeite ich an einem (streng geheimen) Programm, das u.a. mit SQL-Datenbanken kommunizieren soll. Die grundsätzliche Infrastruktur dafür bringt Go von Haus aus mit. Jedoch kann es etwas mühselig sein, bei jeder Abfrage dieselbe Routine (samt Fehlerbehandlung) durchzukauen:
[code]
package blog
import (
"database/sql"
)
type Employee struct {
GivenName, FamilyName string
}
func GetEmployees(db *sql.DB) ([]Employee, error) {
rows, errQuery := db.Query("SELECT given_name, family_name FROM employee")
if errQuery != nil {
return nil, errQuery
}
defer rows.Close()
employees := []Employee{}
for {
if rows.Next() {
row := Employee{}
if errScan := rows.Scan(&row.GivenName, &row.FamilyName); errScan != nil {
return nil, errScan
}
employees = append(employees, row)
} else if errNext := rows.Err(); errNext == nil {
break
} else {
return nil, errNext
}
}
return employees, nil
}
[/code]
Außerdem hat sich in einem vergangenen Projekt herausgestellt, dass (zumindest in Transaktionen) erst nach rows.Close()
die nächste Datenbank-Operation beginnen kann. Dies verpflichtete fast schon dazu, den Code ab defer db.Close()
bei jeder Abfrage so oder so ähnlich zu schreiben. Letztendlich löste das Team das Problem mit folgender Funktion:
[code]
func FetchAll(db *sql.DB, query string, args …interface{}) ([][]interface{}, error)
[/code]
Diese erledigte die oben gezeigte Routinearbeit und verringerte damit den Aufwand pro Abfrage deutlich:
[code]
func GetEmployees(db *sql.DB) ([]Employee, error) {
rows, errFetchAll := FetchAll(db, "SELECT given_name, family_name FROM employee")
if errFetchAll != nil {
return nil, errFetchAll
}
employees := []Employee{}
for _, row := range rows {
employees = append(employees, Employee{row[0].(string), row[1].(string)})
}
return employees, nil
}
[/code]
Jedoch war nun jede Spalte jeder Zeile des Ergebnisses ein interface{}
, das erstmal in den richtigen Datentyp umgewandelt werden musste. Dafür wiederum musste die neue Funktion zusätzlich den Spaltentyp beim Datenbanktreiber erfragen, um immer die (hinter dem interface{}
versteckten) Datentypen zurückzugeben, die die konkrete Abfrage erwartet. Andernfalls hätten wir uns auf die Standard-Datentypen der Datenbanktreiber verlassen müssen.
Lösung
Nun übernehme ich also den Code Schritt für Schritt in das neue Projekt und frage mich: Geht das nicht auch einfacher? Ja, mit sog. Reflection:
[code]
package blog
import (
"database/sql"
"reflect"
)
func FetchAll(db *sql.DB, rowType interface{}, query string, args …interface{}) (interface{}, error) {
rows, errQuery := db.Query(query, args…)
if errQuery != nil {
return nil, errQuery
}
defer rows.Close()
blankRow := reflect.ValueOf(rowType)
res := reflect.MakeSlice(reflect.SliceOf(blankRow.Type()), 0, 0)
idx := -1
scanDest := make([]interface{}, blankRow.NumField())
for {
if rows.Next() {
res = reflect.Append(res, blankRow)
idx++
row := res.Index(idx)
for i := range scanDest {
scanDest[i] = row.Field(i).Addr().Interface()
}
if errScan := rows.Scan(scanDest…); errScan != nil {
return nil, errScan
}
} else if errNext := rows.Err(); errNext == nil {
break
} else {
return nil, errNext
}
}
return res.Interface(), nil
}
[/code]
Diese Funktion erwartet einen zusätzlichen Parameter, rowType
. Dessen eigentlicher Typ hinter interface{}
(Employee
) bestimmt den Typ einer Zeile des Abfrage-Ergebnisses. Das komplette Ergebnis ist logischerweise eine Slice aus Zeilen ([]Employee
). Mit Hilfe von Funktionen aus dem reflect-Paket arbeitet FetchAll()
zur Laufzeit mit dem konkreten Datentyp Employee
, fast so als wäre er mittels Generics zur Kompilierzeit bekannt:
reflect.ValueOf(rowType)
analysiertrowType
und kapselt ihn als Wert vom TypEmployee
reflect.ValueOf(rowType).Type()
steht fürEmployee
reflect.SliceOf(Employee)
steht für[]Employee
reflect.MakeSlice([]Employee, 0, 0)
steht fürmake([]Employee, 0, 0)
reflect.ValueOf(rowType).NumField()
zählt die Felder des StructsEmployee
Ja, richtig, rowType
muss ein Struct sein, sonst stürzt das Programm spätestens bei reflect.ValueOf(rowType).NumField()
ab. Jedes Feld des Structs steht nämlich für eine Spalte des Abfrage-Ergebnisses. Genau das wird in der darauf folgenden Schleife wie folgt bewerkstelligt:
res = reflect.Append(res, reflect.ValueOf(rowType))
steht fürres = append(res, Employee{})
res.Index(idx)
steht fürres[idx]
res[idx].Field(0)
steht fürres[idx].GivenName
res[idx].GivenName.Addr()
steht für&res[idx].GivenName
Und .Interface()
holt letztendlich den Zeiger auf das Struct-Feld aus der Reflection-Versenkung, damit rows.Scan()
die entsprechende Spalte des Abfrage-Ergebnisses darin speichert. Am Ende verbirgt sich hinter res
tatsächlich ein []Employee
, das mit res.Interface()
in ein interface{}
gekapselt, um es zurückzugeben. Damit bestimmt GetEmployees()
den Zeilen-Typ im voraus und schrumpft auf ein vernünftiges Minimum:
[code]
func GetEmployees(db *sql.DB) ([]Employee, error) {
rows, errFetchAll := FetchAll(db, Employee{}, "SELECT given_name, family_name FROM employee")
if errFetchAll != nil {
return nil, errFetchAll
}
return rows.([]Employee), nil
}
[/code]
Fazit
Nachdem ich zuletzt schon eine C-Bibliothek in Go wiederverwendet habe, spare ich schon zum zweiten mal in Folge Code und damit Zeit. Sprich, wir arbeiten jetzt noch ein bisschen effizienter (als sowieso schon) an euren Projekten. Bestelle noch heute!

0 Kommentare