Ein Einstieg in die Programmiersprache Go, Teil 2

Der zweite Teil der Artikelserie zu Go widmet sich unter anderem der Fehlerbehandlung und der Standardbibliothek, und auch negative Aspekte von Go kommen zur Sprache.

Sprachen  –  56 Kommentare
Ein Einstieg in die Programmiersprache Go, Teil 2

(Bild: iX)

Der erste Teil des Go-Artikels hat sich auf Sprachfeatures wie Go-Routinen, Channels und Objektorientiertung konzentriert. Er zeigte, wie es den Go-Autoren gelungen ist, eine breit einsetzbare Sprache zu kreieren, die leicht erlernbar ist.

Der folgende Teil beschäftigt sich zunächst damit, wie Go die Fehlerbehandlung löst. Anschließend verlässt der Beitrag die Sprachenebene, um die Standardbibliothek, Cross-Compiling und Dependency-Management zu betrachten und einige Schwachpunkte der Sprache zu diskutieren.

Error-Handling in Go

Produktionsreife Programme bestehen oft zu einem großen Teil aus Fehlerbehandlung, unabhängig von der Programmiersprache. Go nutzt dazu Multivalue Returns, Early Returns und defer-Statements. Mit Multivalue Returns ist es leicht, einen Fehler in einer Funktion oder Methode zu signalisieren. Neben den eigentlichen Rückgabewerten gibt Go immer noch einen weiteren Wert zurück, der das Interface error implementiert. Ist er nil, gab es keinen Fehler bei der Ausführung der Funktion beziehungsweise Methode, und Rückgabewerte sind bedenkenlos einsetzbar. Sollte er jedoch nicht nil sein, müssen Entwickler den Fehler beheben und die Rückgabewerte als invalide betrachten.

Operationen auf Dateien sind typischerweise anfällig für allerlei Fehler – Dateien können zum Beispiel nicht vorhanden, zu öffnen oder beschreibbar sein. All das muss man bei der Programmierung berücksichtigen. In Go sieht das wie folgt aus:

// versuche eine Datei zu öffnen
// im Erfolgsfall ist file ein Dateihandle und err ist nil
file, err := os.OpenFile(filename, os.O_RDWR, 0755)
// prüfe, ob das Öffnen erfolgreich war
if err != nil {
// das Öffnen war nicht erfolgreich
// der normale Programmfluss kann nicht fortgesetzt werden
return err
}
// das Öffnen war erfolgreich
// file kann genutzt werden, um zum Beispiel in die Datei zu schreiben
n, err := file.WriteString("Hello Go!")
// prüfe, ob das Schreiben erfolgreich war
if err != nil {
return err
}
fmt.Println("Wrote", n, "bytes to file:", filename)
file.Close()

Obiges Listing hat noch ein schwerwiegendes Problem. Wenn beim Schreiben in die Datei ein Fehler auftritt, verlässt es die Funktion beziehungsweise Methode vorzeitig im if-Block (early return). Die Zeile, die die Datei schließt (file.Close()), wird nicht mehr erreicht und es kommt zu einem Ressourcen-Leak. Genau deshalb sind Early Returns in Sprachen ohne automatisches Speichermanagement und try-catch-finally-Konstrukte verpönt. In C nutzt man ein goto, um zum Ende der Funktion zu springen und dort die Ressourcen aufzuräumen und auch nur dort die Funktion zu verlassen (one-point return). In Java wäre die Fehlerbehandlung in einem try-catch-Block angesiedelt, während ein finally-Block die Ressourcen aufräumt. Ein Early Return wäre möglich, aber es ist umstritten, ob das der richtige Stil ist.

Idiomatische Early Returns

In Go sind Early Returns idiomatisch, das heißt, man benötigt einen Weg, um Ressourcen wieder aufzuräumen. Im Beispiel könnte das noch von Hand im if-Block passieren, aber sobald mehrere Ressourcen im Spiel sind, wird es schnell unübersichtlich. Deshalb gibt es in Go das defer-Statement. Es kann Funktionen oder Methoden nach einem return-Statement ausführen. Im Beispiel sieht das wie folgt aus:

// versuche, eine Datei zu öffnen
// im Erfolgsfall ist file ein Dateihandle und err ist nil
file, err := os.OpenFile(filename, os.O_RDWR, 0755)
// prüfe, ob das Öffnen erfolgreich war
if err != nil {
// das Öffnen war nicht erfolgreich
// der normale Programmfluss kann nicht fortgesetzt werden
return
}
// das Öffnen war erfolgreich
// file kann genutzt werden, um zum Beispiel in die Datei zu schreiben
// Schließe die Datei beim Verlassen der Funktion
defer file.Close()
n, err := file.WriteString("Hello Go!")
// prüfe, ob das Schreiben erfolgreich war
if err != nil {
return
}
fmt.Println("Wrote", n, "bytes to file:", filename)

Nach dem erfolgreichen Öffnen der Datei merkt defer file.Close() das Schließen der Datei vor. defer sorgt dafür, dass file.Close() beim Verlassen der Funktion mit return ausführt, egal mit welchem return man die Funktion verlässt. Sollten mehrere Aufräumaktionen notwendig sein, können Entwickler weitere defers benutzen. Beim Verlassen der Funktion erfolgt die Abarbeitung nach dem Prinzip Last-In-First-Out.

Im Gegensatz zu einer Fehlerbehandlung mit Exceptions ist die Fehlerbehandlung per Rückgabewert repetitiv. Nutzer können aber sofort sehen, wo ein Fehler auftreten kann, und es existiert keine Möglichkeit für einen versteckten Kontrollfluss, wie es bei Exceptions der Fall ist. Mit defer und Early Returns stehen darüber hinaus zusammenhängende Teile immer dicht beisammen und nicht über den Code verteilt. Im Beispiel sind das Öffnen, die eventuell benötigte Fehlerbehandlung und das Schließen der Datei direkt untereinander positioniert. Dadurch ist der Code insgesamt leicht lesbar.