Select Page

Generics waren gestern. Lang lebe Golangs Reflection!

by | May 2, 2019 | Golang

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) analysiert rowType und kapselt ihn als Wert vom Typ Employee
  • reflect.ValueOf(rowType).Type() steht für Employee
  • reflect.SliceOf(Employee) steht für []Employee
  • reflect.MakeSlice([]Employee, 0, 0) steht für make([]Employee, 0, 0)
  • reflect.ValueOf(rowType).NumField() zählt die Felder des Structs Employee

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ür res = append(res, Employee{})
  • res.Index(idx) steht für res[idx]
  • res[idx].Field(0) steht für res[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!

Alexander Klimov
Alexander Klimov
Senior Developer

Alexander hat 2017 seine Ausbildung zum Developer bei NETWAYS erfolgreich abgeschlossen. Als leidenschaftlicher Programmierer und begeisterter Anhänger der Idee freier Software, hat er sich dabei innerhalb kürzester Zeit in die Herzen seiner Kollegen im Development geschlichen. Wäre nicht ausgerechnet Gandhi sein Vorbild, würde er von dort aus daran arbeiten, seinen geheimen Plan, erst die Abteilung und dann die Weltherrschaft an sich zu reißen, zu realisieren - tut er aber nicht. Stattdessen beschreitet er mit der Arbeit an Icinga Web 2 bei uns friedliche Wege.

0 Comments

Submit a Comment

Your email address will not be published. Required fields are marked *

More posts on the topic Golang

check_prometheus ist jetzt öffentlich verfügbar!

Monitoring ist komplex, das wissen wir hier bei NETWAYS leider zu gut. Deswegen laufen in der Infrastruktur auch mal gerne mehrere Tools für die Überwachung. Zwei gern gesehene Kandidaten sind dabei Icinga und Prometheus. Icinga und Prometheus erfüllen...

Neues zum go-check

Lang ist es her, dass ein Blogpost über das hauseigene NETWAYS go-check geschrieben wurde. Seitdem hat sich das go-check immer weiterentwickelt und wurde mit vielen verschiedenen Funktionen erweitert, sodass die Pluginentwicklung noch einfacher von der Hand geht. Um...