Go Home Page
Die Programmiersprache Go

Effective Go — Deutsche Übersetzung

Das Original:
http://golang.org/doc/effective_go.html
© 2009-12 The Go Authors
Except as noted, this content is licensed under Creative Commons Attribution 3.0.
Diese Übersetzung:
http://www.bitloeffel.de/DOC/golang/effective_go_20120214_de.html (Stand: 14.02.2012)
© 2010-12 Hans-Werner Heinzen @ Bitloeffel.de
Die Nutzung dieses Dokuments ist unter den Bedingungen von Creative Commons Namensnennung 3.0 erlaubt. Für die verlinkten Quelldateien gelten andere Bestimmungen.
Für die Fachbegriffe gibt es hier noch eine Wörterliste.

Wirkungsvoll Go programmieren

Einleitung

Go ist eine neue Programmiersprache. Wenn sie auch Anleihen bei existierenden Sprachen macht, so hat sie doch so ungewöhnliche Eigenschaften, dass der Charakter effektiver Go-Programme sich unterscheidet von dem der in verwandten Sprachen geschriebenen Programme. Eine direkte Übersetzung aus C++ oder Java nach Go wird kaum zufriedenstellen; Java-Programme sind in Java geschrieben, nicht in Go. Überdenkt man dagegen das Problem aus einer Go-Perspektive, kann ein gelungenes aber völlig anderes Programm dabei herauskommen. Anders gesagt: um gutes Go zu schreiben, braucht es das Verständnis seiner Eigenarten und typischen Programmiermuster. Und wichtig sind außerdem die eingeführten Konventionen zur Namensgebung, zur Formatierung, zur Programmkonstruktion usw, damit Ihre Programme auch für andere Go-Programmierer leicht zu verstehen sind.

Dieses Dokument gibt Tipps, wie man klaren, typischen Go-Kode schreibt. Es baut auf die Sprachbeschreibung und die Programmieranleitung auf; beide sollten Sie vorher lesen.

Beispiele

Die Go-Paketquellen sind nicht nur Kernbibliothek; sie sind auch Beispiele dafür, wie man diese Sprache benutzt. Wenn Sie Fragen haben zur Herangehensweise an ein Problem oder wie man etwas implementieren könnte: hier finden Sie Antworten und Ideen und Hintergrundwissen.

Formatierung

Formatierungsregeln sind höchst umstritten und selten stringent. Programmierer können sich an verschiedene Formatierstile anpassen, doch besser wär's, wenn sie das nicht müssten. Weniger Zeit müsste man diesem Thema widmen, würden alle sich an einen Stil halten. Doch wie nähern wir uns diesem Utopia ohne langatmige Kodier-Richtlinien.

Für Go benutzen wir einen ungewöhnlichen Ansatz: wir überlassen das Formatieren der Maschine. Das Programm gofmt — aufrufbar auch über go tool fmt: hier wird statt auf Datei- auf Paketebene gearbeitet — liest das Go-Quellprogramm und gibt den Kode zurück mit standardisierten Einrückungen und vertikaler Ausrichtung; die Kommentare werden beibehalten und, wenn nötig, reformatiert. Wenn Ihnen eine neue Layout-Konstellation begegnet und Sie wissen nicht, wie man sie handhabt: lassen Sie gofmt laufen. Wenn das Ergebnis nicht richtig aussieht, ordnen Sie Ihren Kode um (oder melden Sie einen gofmt-Fehler), nur: arbeiten Sie nicht drum herum.

Beispielweise ist es unnötig, die Kommentare zu den Feldern einer Struktur auszurichten. Gofmt tut das für Sie. Bei folgender Eingabe:

type T struct {
    name string // name of the object
    value int // its value
}
		

wird gofmt die Spalten so ausrichten:

type T struct {
    name  string // name of the object
    value int    // its value
}
		

Der Go-Quellkode aller Standardpakete ist mit gofmt formatiert worden.

Hier noch die fehlenden Details in aller Kürze:

Einrückungen
Dafür benutzen wir Tabulatorzeichen, und gofmt gibt solche standardmäßig aus. Leerzeichen? Nur, wenn nötig!
Zeilenlänge
Dafür gibt es kein Limit in Go: also keine Angst vor Zeilenüberlauf. Sieht eine Zeile zu lang aus, brechen Sie um und rücken mit einem Extra-Tab ein.
Runde Klammern
Go braucht nur wenige. Kontrollanweisungen (if, for, switch) kommen ganz ohne aus. Außerdem ist die Rangreihenfolge der Operatoren kürzer und klarer:
x<<8 + y<<16
bedeutet das, was hier die Leerzeichen andeuten.

Kommentare

Go bietet /* */ Block-Kommentare im C-Stil und // Zeilenkommentare im C++-Stil. Zeilenkommentare sind die Norm. Blockkommentare sieht man zumeist als Paket-Beschreibung; nützlich sind sie außerdem, um große Kode-Strecken auszukommentieren.

Das Programm godoc — es ist gleichzeitig Webserver — verarbeitet Go-Quelldateien und extrahiert Kommentare für die Dokumentation der Pakete. Kommentare, die direkt vor Deklarationen der obersten Ebene stehen, also nicht durch Leerzeilen davon getrennt sind, werden als erklärender Text zusammen mit der Deklaration extrahiert. Inhalt und Stil dieser Kommentare bestimmen also die Qualität der von godoc generierten Dokumentation.

Jedes Paket sollte einen Paket-Kommentar haben, d.h. einen Blockkommentar vor der package-Klausel. Pakete mit mehreren Dateien brauchen ihn nur in einer Datei, egal in welcher. Der Paket-Kommentar soll einen Überblick geben über das Paket als Ganzes. Auf der von godoc generierten Seite erscheint er zuerst und soll auf die dann folgenden Detailinformationen vorbereiten.

/*
    Das Paket regexp implementiert eine einfache Bibliothek für
    reguläre Ausdrücke.

    Die folgende Syntax wird akzeptiert:

    Regexp:
        Verkettung { '|' Verkettung }
    Verkettung:
        { Abschluss }
    Abschluss:
        Ausdruck [ '*' | '+' | '?' ]
    Ausdruck:
        '^'
        '$'
        '.'
        Zeichen
        '[' [ '^' ] Zeichen-Bereich ']'
        '(' Regexp ')'
*/
package regexp
		

Für ein einfaches Paket kann der Paket-Kommentar kurz sein:

// Das Paket path implementiert Hilfsroutinen
// zum Manipulieren von Schrägstrich-Pfadnamen.
		

Kommentare brauchen keine Extras wie sternverzierte Rahmen. Die generierte Ausgabe benutzt vielleicht noch nicht mal eine Schriftart mit fester Zeichenbreite: lassen Sie also das Ausrichten durch Leerzeichen sein — wie gofmt kümmert sich auch godoc selbst darum. Schließlich: Kommentare sind Text und werden nicht interpretiert. HTML- oder Markierungen wie _diese_ werden Zeichen für Zeichen wiedergegeben — also Finger weg auch davon.

Innerhalb eines Pakets dient jeder Kommentar direkt vor einer Deklaration der obersten Ebene als Doc-Kommentar dieser Deklaration. Zu jedem exportierten (großgeschriebenen) Namen sollte es einen solchen Doc-Kommentar geben.

Als Doc-Kommentare eignen sich ganze Sätze am besten, weil sie ein breites Spektrum automatisch erzeugter Darstellungen erlauben. Der erste sollte ein Ein-Satz-Resümee sein und mit dem deklarierten Namen beginnen.

// Compile durchsucht einen regulären Ausdruck und gibt im Erfolgsfall
// ein Regexp-Objekt zurück, das mit einem Text abgeglichen werden kann.
func Compile(str string) (regexp *Regexp, error error) {
		

Die Deklarationssyntax von Go erlaubt das Zusammenfassen zu Gruppen. Ein einzelner Doc-Kommentar kann eine Gruppe zusammengehöriger Konstanten oder Variablen einleiten. Und er darf knapp bleiben, weil ja die gesamte Deklaration präsentiert wird.

// Fehler, die beim Parsen auftreten können.
var (
    ErrInternal      = errors.New("regexp: interner Fehler")
    ErrUnmatchedLpar = errors.New("regexp: unpaariges '('")
    ErrUnmatchedRpar = errors.New("regexp: unpaariges ')'")
    ...
)
		

Auch bei nicht exportierten Namen kann eine Gruppierung die Zusammengehörigkeit unterstreichen, also beispielsweise andeuten, dass eine Variablengruppe durch ein Mutex geschützt wird

var (
    countLock   sync.Mutex
    inputCount  uint32
    outputCount uint32
    errorCount  uint32
)
		

Namen

Namen sind wichtig! — in Go wie in jeder anderen Sprache. Manchmal haben sie sogar semantische Bedeutung. Zum Beispiel wird die Sichtbarkeit eines Namens außerhalb eines Go-Pakets dadurch festgelegt, dass der erste Buchstabe großgeschrieben wird. Es ist also sinnvoll, sich eine Weile mit Namenskonventionen in Go zu beschäftigen.

Paketnamen

Wenn ein Paket importiert wurde, wird über den Paketnamen auf die Inhalte zugegriffen. Nach:

import "bytes"
		

kann das importierende Programm von bytes.Buffer sprechen. Es ist hilfreich, wenn jeder Nutzer eines Pakets denselben Namen benutzen kann, woraus folgt, dass dieser Name gut sein sollte, nämlich kurz, treffend, ansprechend. Konvention: Pakete haben ein kleingeschriebenes Wort als Namen; Unterstriche oder Binnenmajuskeln sollten nicht nötig sein. Und, lieber zu kurz als zu lang, weil jeder der Ihr Paket benutzt, den Namen eintippen muss. Und denken sie erstmal nicht über Namenskonflikte nach, denn der Paketname ist nur die Vorgabe; er muss nicht über den gesamten Quellkode eindeutig sein. In dem seltenen Fall eines Konflikts kann man lokal einen anderen Namen verwenden. Und überhaupt gibt es kaum Grund zur Verwirrung, weil der Dateiname in der Import-Anweisung festlegt, welches Paket genau benutzt wird.

Denn nach einer weiteren Konvention ist der Paketname der Basisname seines Quellverzeichnisses; das Paket in src/pkg/encoding/base64 wird als encoding/base64 importiert und hat als Namen base64, nicht encoding_base64 und auch nicht encodingBase64.

Der Importeur des Pakets wird mit dem Namen auf dessen Inhalte bezugnehmen (die Schreibweise import . ist hauptsächlich für Tests gedacht und sollte möglichst vermieden werden), so dass die vom Paket exportierten Namen auf eigenes Gestottere verzichten können. Zum Beispiel heißt im Paket bufio der Typ des gepufferten Lesers Reader und nicht BufReader, weil Benutzer ihn als bufio.Reader ansprechen, welches ein klarer, treffender Name ist.

Und weil Importiertes immer mit dem Paketnamen adressiert wird, kollidiert auch bufio.Reader nicht mit io.Reader. Ähnlich bei der Funktion die neue Instanzen von ring.Ring erzeugt (das ist die Go-Version eines Konstruktors): normalerweise würde man NewRing rufen, aber weil Ring der einzige exportierte Typ des Pakets ist und weil das Paket ring heißt, heißt die Funktion nur New. Klienten des Pakets sehen sie als ring.New. Lassen Sie sich von unserer Paketstruktur zu guten Namen inspirieren.

Und noch ein kurzes Beispiel: once.Do;   once.Do(setup) ist gut zu lesen und würde nichts gewinnen, wenn man once.DoOrWaitUntilDone(setup) schriebe. Lange Namen sind nicht automatisch lesbarer. Wenn der Name für etwas Kompliziertes oder Hintergründiges steht, ist es gewöhnlich besser, einen hilfreichen Doc-Kommentar zu schreiben, als alle Information in den Namen zu packen.

Getter

Go unterstützt Get- und Set-Methoden nicht automatisch. Es spricht aber auch nichts dagegen, dass Sie solche selbst zur Verfügung stellen; oft genug ist das die angemessene Vorgehensweise. Es ist aber weder typischer Go-Stil noch ist es überhaupt notwendig, die Get-Methoden Get... zu nennen. Wenn es eine Variable owner (klein geschrieben, nicht exportiert) gibt, dann sollte die Get-Methode Owner (groß geschrieben, exportiert) und nicht GetOwner heißen. Anhand des ersten, großgeschriebenen Buchstabens lässt sich die Methode von der Variablen unterscheiden. Die Set-Methode wird man hingegen wahrscheinlich SetOwner nennen. Beides ist gut lesbar:

owner := obj.Owner()
if owner != user {
    obj.SetOwner(user)
}
		

Interface-Namen

Konvention: Interfaces mit nur einer Methode werden benannt durch den Methodenamen mit angehängtem er: Reader, Writer, Formatter usw.

Es gibt eine ganze Reihe solcher Namen und es lohnt sich, sie und die enthaltenen Funktionsnamen zu respektieren. Read, Write, Close, Flush, String und so weiter haben anerkannte Signaturen und Bedeutungen. Stiften Sie also keine Verwirrung und benutzen Sie solche Namen nur dann für eigene Methoden, wenn diegleiche Signatur und Bedeutung vorliegt. Umgekehrt, wenn Ihr Typ eine Methode mit dergleichen Signatur und Bedeutung implementiert wie die Methode eines wohlbekannten Typs, nehmen Sie dengleichen Namen und diegleiche Signatur; nennen Sie also ihre String-Konverter-Methode String, nicht ToString.

Groß-/Kleinschreibung

Schließlich noch diese Konvention: In Go schreiben wir mehrteiligen Wörter ohne Unterstriche GrossGross oder kleinGross.

Semikolon

Wie bei C so auch bei Go benutzt die formale Grammatik Semikola zum Terminieren von Anweisungen; anders als bei C erscheinen sie aber nicht im Kode. Stattdessen fügt der Lexer sie nach einer einfachen Regel ein; der Eingabetext selbst kommt fast ohne aus.

Die Regel geht so: Wenn das letzte Syntaxelement vor dem Zeilenende ein Bezeichner (dazu gehören auch Wörter wie int und float64), ein Literal (eine Zahl oder eine String-Konstante) oder eines aus der Liste

break continue fallthrough return ++ -- ) }
		

ist, dann fügt der Lexer ein Semikolon an. Das lässt sich so zusammenfassen: "Folgt das Zeilenende einem Syntaxelement, welches eine Anweisung beendet, dann füge ein Semikolon ein."

Das Semikolon kann man sich auch vor einer schließenden geschweiften Klammer sparen:

    go func() { for { dst <- <-src } }()
		

kommt ohne Semikolon aus. Typische Go-Programme haben Semikola nur in Konstrukten wie der for-Schleife, um Startschritt, Bedingung und Zählschritt zu trennen. Man braucht sie auch, um mehrere Anweisungen pro Zeile zu trennen ... sollten Sie wirklich so programmieren wollen.

Aber Vorsicht! Niemals sollten Sie die öffnende geschweiften Klammer einer Kontrollstruktur (if, for, switch, oder select) in die nächste Zeile setzen. Dann würde ein Semikolon vor der geschweiften Klammer eingefügt mit vielleicht ungewollten Effekten. Schreiben Sie so:

if i < f() {
    g()
}
		

und nicht so:

if i < f() // falsch!
{          // falsch!
    g()
}
		

Kontrollstrukturen

Die Kontrollstrukturen in Go sind zwar verwandt mit denen in C, aber die Unterschiede sind wichtig. Es gibt keine do- und keine while-Schleifen, nur ein leicht verallgemeinertes for. switch ist flexibler. if und switch akzeptieren wie for einen optionalen Startschritt. Und es gibt zusätzliche Kontrollstrukturen wie den Typ-switch oder den Mehrwege-Kommunikations-Multiplexer select. Auch die Syntax unterscheidet sich etwas: es gibt keine runden Klammern und der Anweisungsrumpf gehört zwischen geschweifte Klammern.

If

Ein simples if sieht in Go so aus:

if x > 0 {
    return y
}
		

Die vorgeschriebenen geschweiften Klammern ermutigen dazu, eine einfache if-Anweisung auf mehrere Zeilen zu verteilen. Das ist guter Programmierstil, insbesondere, wenn der Rumpf Kontrollanweisungen enthält, wie return oder break.

Da if und switch jetzt auch einen Startschritt kennen, ist es üblich, damit eine lokale Variable anzulegen:

if err := file.Chmod(0664); err != nil {
    log.Print(err)
    return err
}
		

In den Go-Bibliotheken wird Ihnen auffallen, dass jedes überflüssige else weggelassen wurde, wenn das if nicht in den darauffolgenden Kode mündet, also der Anweisungsrumpf mit break, continue, goto, oder return endet:

f, err := os.Open(name)
if err != nil {
    return err
}
codeUsing(f)
		

Es folgt ein Beispiel einer üblichen Situation, in der sich der Kode gegen eine ganze Reihe möglicher Fehler schützen muss. Dann ist der Kode gut zu lesen, wenn der Kontrollfluss im Erfogsfall von oben nach unten verläuft, und die Fehler behandelt werden, sobald sie auftreten. Weil Fehlerzweige gewöhnlich mit return enden, kommt dieser Kode ganz ohne else aus:

f, err := os.Open(name)
if err != nil {
    return err
}
d, err := f.Stat()
if err != nil {
    f.Close()
    return err
}
codeUsing(f, d)
		

Redeklaration

Eine Anmerkung: Das letzte Beispiel im vorhergehenden Abschnitt verdeutlicht einen Aspekt der Arbeitsweise der Kurzdeklaration :=. Die Deklaration, die os.Open ruft, lautet:

f, err := os.Open(name)
		

Diese Anweisung deklariert zwei Variablen, f und err. Ein paar Zeilen weiter, beim Rufen von f.Stat heißt es:

d, err := f.Stat()
		

Und das sieht so aus, als ob hier d und err deklariert würden. Aber Achtung: err erscheint in beiden Anweisungen. Diese Verdopplung ist erlaubt: err wird mit der ersten Anweisung deklariert, in der zweiten Anweisung wird aber nur zugewiesen. Also benutzt der Aufruf von f.Stat das bereits existierende err und gibt ihm nur einen neuen Inhalt.

Eine :=-Deklaration darf eine bereits deklarierte Variable v unter den folgenden Bedingungen enthalten:

Diese ungewöhnliche Eigenschaft hat rein pragmatische Gründe. Sie ermöglicht das Nutzen nur einer Variablen err beispielsweise in einer langen Reihe von if-else. Sie werden das oft zu sehen bekommen.

For

Die Go-for-Schleife ist der von C ähnlich, aber nicht gleich. Sie vereint for und while; ein do-while gibt es nicht. Es gibt drei Formen, von denen nur eine Semikola benutzt:

// wie in C das for
for init; condition; post { }

// wie in C das while
for condition { }

// wie in C das for(;;)
for { }
		

Eine Kurzdeklaration erleichtert es, die Laufvariable direkt in der Schleife zu deklarieren.

sum := 0
for i := 0; i < 10; i++ {
    sum += i
}
		

Ob Sie ein Array abarbeiten, ein Slice, einen String oder eine Map, oder ob Sie aus einem Channel lesen, überall dort kann die range-Klausel die Schleife managen.

var m map[string]int
sum := 0
for _, value := range m { // key wird nicht benutzt
    sum += value
}
		

Bei Strings nimmt Ihnen range noch mehr Arbeit ab: es bricht die einzelnen Unicode-Zeichen in UTF-8-Kodierung auf; fehlerhafte Kodierungen werden byteweise betrachtet und jeweils durch die Rune U+FFFD ersetzt. Die Schleife:

for pos, char := range "日本語" {
    fmt.Printf("Zeichen %c beginnt an Byteposition %d\n", char, pos)
}
		

druckt:

Zeichen 日 beginnt an Byteposition 0
Zeichen 本 beginnt an Byteposition 3
Zeichen 語 beginnt an Byteposition 6
		

Und schließlich: Go kennt keinen Komma-Operator, und ++ und -- sind Anweisungen und keine Ausdrücke. Deshalb können und sollten Sie Mehrfachzuweisung nutzen, wenn Ihre for-Schleife mehrere Laufvariablen hat.

// a umdrehen
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
    a[i], a[j] = a[j], a[i]
}
		

Switch

switch ist in Go allgemeiner als in C; die Ausdrücke müssen keine Konstanten sein, geschweige denn Ganzzahlen; die Fälle werden von oben nach unten ausgewertet bis eine Übereinstimmung festgestellt wird; switch ohne Ausdruck bedeutet switch true. Daher ist es möglich — und Go-typisch — statt einer if-else-if-else-Kette ein switch zu benutzen.

func unhex(c byte) byte {
    switch {
    case '0' <= c && c <= '9':
        return c - '0'
    case 'a' <= c && c <= 'f':
        return c - 'a' + 10
    case 'A' <= c && c <= 'F':
        return c - 'A' + 10
    }
    return 0
}
		

Es gibt kein automatisches "Fall-through", aber ein case kann eine Liste von durch Kommas getrennten Werte haben:

func shouldEscape(c byte) bool {
    switch c {
    case ' ', '?', '&', '=', '#', '+', '%':
        return true
    }
    return false
}
		

Hier folgt eine Vergleichsroutine für Byte-Arrays mit zwei switch-Anweisungen:

// Compare vergleicht zwei Byte-Arrays lexikografisch 
// und gibt eine Ganzzahl zurück:
// 0 wenn a==b, -1 wenn a < b, +1 wenn a > b
func Compare(a, b []byte) int {
    for i := 0; i < len(a) && i < len(b); i++ {
        switch {
        case a[i] > b[i]:
            return 1
        case a[i] < b[i]:
            return -1
        }
    }
    switch {
    case len(a) < len(b):
        return -1
    case len(a) > len(b):
        return 1
    }
    return 0
}
		

Ein Switch kann außerdem benutzt werden, um dynamisch den Typ einer Interface-Variablen zu bestimmen. Dieser Typ-Switch benutzt die Syntax der Typzusicherung mit dem Schlüsselwort type in runden Klammern. Wenn Switch in seinem Ausdruck eine Variable deklariert, wird die Variable in jeder Klausel diesen Typ haben.

switch t := interfaceValue.(type) {
default:
    fmt.Printf("unbekannter Typ %T", t) // %T druckt den Typ
case bool:
    fmt.Printf("bool %t\n", t)
case int:
    fmt.Printf("int %d\n", t)
case *bool:
    fmt.Printf("Zeiger auf ein bool %t\n", *t)
case *int:
    fmt.Printf("Zeiger auf ein int %d\n", *t)
}
		

Funktionen

Multiple Rückgabewerte

Eine der ungewöhnlichen Eigenschaften von Go ist, dass Funktionen und Methoden mehrere Werte zurückgeben können. Mit dieser Schreibweise kann man eine paar in C übliche, aber umständliche, Programmiermuster verbessern: den Missbrauch der Rückgabevariablen zur Aufnahme eines Fehlerwerts (wie -1 für EOF), oder das Verändern mitgegebener Argumente.

In C wird ein Schreibfehler durch einen negative Zähler gemeldet, während der Fehlerkode still und leise in eine ungeschützte Variable wandert. In Go gibt Write einen Zähler und einen Fehler zurück: "Ja, ein paar Bytes wurden geschrieben, aber nicht alle, weil das Zielgerät voll ist." Die Signatur von *File.Write im Paket os ist:

func (file *File) Write(b []byte) (n int, err error)
		

Die Dokumentation sagt: Es gibt die Anzahl der geschriebenen Bytes zurück und einen error ungleich nil, wenn n != len(b). Das ist üblicher Stil; für weitere Beispiele siehe den Abschnitt über Fehlerbehandlung.

Ein solcher Ansatz macht es unnötig, Zeiger als Referenzparameter zu benutzen. Hier eine naive Fuktion, die sich ab einer Position eine Zahl aus einem Byte-Array greift, und diese und die Position dahinter zurückgibt:

func nextInt(b []byte, i int) (int, int) {
    for ; i < len(b) && !isDigit(b[i]); i++ {
    }
    x := 0
    for ; i < len(b) && isDigit(b[i]); i++ {
        x = x*10 + int(b[i])-'0'
    }
    return x, i
}
		

Die könnte Sie benutzen, um ein Eigabe-Array a nach Zahlen abzusuchen:

    for i := 0; i < len(a); {
        x, i = nextInt(a, i)
        fmt.Println(x)
    }
		

Benannte Ergebnisparameter

Den Rückgabe- oder Ergebnisparametern einer Go-Funktion kann man Namen geben, und sie benutzen wie reguläre Variablen — genauso wie die Eingabeparameter auch. Wenn sie Namen haben, werden sie beim Funktionsaufruf mit ihrem jeweiligen, zum Typ passenden Null-Wert vorbelegt; eine return-Anweisung ohne Argumente benutzt die aktuellen Werte der Ergebnisparameter als Rückgabewerte.

Die Namen sind nicht notwendig, aber sie können den Kode kürzer und klarer machen: sie dokumentieren! Wenn wir den Ergebnissen von nextInt Namen geben, wird sofort offensichtlich, welches zurückgegebene int für was steht.

func nextInt(b []byte, pos int) (value, nextPos int) {
		

Weil benamste Ergebnisparameter initialisiert sind und mit dem nackten return verknüpft, können sie sowohl vereinfachen als auch klarstellen. Hier ein Kodestück aus io.ReadFull, welches sie klug nutzt:

func ReadFull(r Reader, buf []byte) (n int, err error) {
    for len(buf) > 0 && err == nil {
        var nr int
        nr, err = r.Read(buf)
        n += nr
        buf = buf[nr:len(buf)]
    }
    return
}
		

Defer

Die defer-Anweisung [deutsch: aufschieben, zurückstellen, A.d.Ü.] merkt einen Funktionsaufruf vor, und zwar für die Ausführung unmittelbar bevor die Funktion, die das defer enthält, endet. Das ist ungewohnt, aber effektiv für Situationen, in denen Ressourcen freigegeben werden müssen, egal welchen Weg zum return die Verarbeitung nimmt. Typische Beispiele sind das Freigeben eines Mutex' und das Schließen einer Datei.

// Contents gibt den Inhalt einer Datei als String zurück.
func Contents(filename string) (string, error) {
    f, err := os.Open(filename)
    if err != nil {
        return "", err
    }
    defer f.Close() // f.Close soll laufen, wenn wir fertig sind

    var result []byte
    buf := make([]byte, 100)
    for {
        n, err := f.Read(buf[0:])
        result = append(result, buf[0:n]...) // append wird weiter unten erläutert
        if err != nil {
            if err == io.EOF {
                break
            }
            return "", err // f wird geschlossen, wenn wir hier enden
        }
    }
    return string(result), nil // f wird geschlossen, wenn wir hier enden
}
		

Das Aufschieben eines Funktionsaufrufs wie Close hat zweierlei Vorteile. Erstens garantiert es, dass das Dateischließen nicht vergessen wird — ein beliebter Fehler, wenn bei einer Änderung ein weiteres return hinzukommt. Zweitens bewirkt es, dass das Close nahe beim Open steht; das ist viel klarer, als wenn es am Ende der Funktion stünde.

Die Argumente für die zurückgestellte Funktion — wozu auch das Empfängerobjekt gehört, wenn es sich um eine Methode handelt — werden bereits beim defer ausgewertet, und nicht erst beim Funktionsaufruf. Abgesehen davon, dass man sich Sorgen über geänderte Variableninhalte zum Ausführungszeitraum sparen kann, heißt das, dass der Aufruf einer Funktion mehrmals zurückgestellt werden kann. Hier ein etwas albernes Beispiel:

for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
}
		

Aufgeschobene Funktionen werden in LIFO-Reihenfolge ausgeführt, so dass dieser Kode die Ausgabe 4 3 2 1 0 erzeugt, sobald die übergeordnete Funktion endet. Ein sinnvolleres Beispiel ist diese simple Methode, die Ausführung von Funktionen zu protokollieren. Zwei einfache Trace-Routinen können wir so schreiben:

func trace(s string)   { fmt.Println("Anfang:", s) }
func untrace(s string) { fmt.Println("Ende:", s) }

// So werden sie benutzt:
func a() {
    trace("a")
    defer untrace("a")
    // irgendwas tun....
}
		

Wir können das aber noch besser! Wir können die Tatsache ausnutzen, dass Argumente für die zurückgestellte Funktion ja schon beim defer ausgewertet werden. Die Trace-Routine kann das Argument für die Untrace-Routine bereitstellen:

func trace(s string) string {
    fmt.Println("Anfang:", s)
    return s
}

func un(s string) {
    fmt.Println("Ende:", s)
}

func a() {
    defer un(trace("a"))
    fmt.Println("in a")
}

func b() {
    defer un(trace("b"))
    fmt.Println("in b")
    a()
}

func main() {
    b()
}
		

druckt:

Anfang: b
in b
Anfang: a
in a
Ende: a
Ende: b
		

Programmierern, die von anderen Sprachen her an die Blockbindung der Ressourcen gewöhnt sind, mag defer seltsam erscheinen. Aber gerade daraus, dass es sich nicht an Blöcken, sondern an Funktionen orientiert, erwachsen seine interessantesten und wirkungsvollsten Anwendungen. Im Abschnitt über panic und recover werden wir ein weiteres Beispiel sehen.

Daten

Speicherzuteilung mit new

Go kennt zwei Primitive für Speicherreservierung, die eingebauten Funktionen [englisch: built-in, A.d.Ü.] new und make. Diese arbeiten unterschiedlich und sind jeweils für andere Typen zuständig — verwirrend zunächst, aber die Regeln sind einfach. Nehmen wir uns zuerst new vor. Das ist eine eingebaute Funktion, welche Speicher reserviert, doch anders als ihre Namensvettern in einigen anderen Sprachen initialisiert sie den Speicher nicht: sie "nullisiert" ihn. Das heißt new(T) reserviert mit Null vorbelegten Speicher für eine neue Instanz von T und gibt die Adresse, einen Wert vom Typ *T, zurück. Im Go-Sprachgebrauch: Es gibt einen Zeiger zurück auf einen neuen Nullwert vom Typ T.

Und weil new "nullisierten" Speicher zurückgibt, ist es hilfreich, wenn Sie beim Design von Datenstrukturen dafür sorgen, dass der Nullwert jedes Typs ohne weitere Initialisierung benutzt werden kann. Soll heißen: der Nutzer der Datenstruktur kann eine solche mit new erzeugen und dann gleich loslegen. Zum Beispiel sagt die Dokumentation zu bytes.Buffer: "der Nullwert von Buffer ist ein einsatzbereiter, leerer Puffer". Ähnlich beim sync.Mutex: Er hat weder einen expliziten Konstruktor noch eine Init-Methode; stattdessen ist der Nullwert eines sync.Mutex definiert als ungesperrter Mutex.

Diese nützliche Nullwert-Eigenschaft ist sogar transitiv. Betrachten sie folgende Typdeklaration:

type SyncedBuffer struct {
    lock    sync.Mutex
    buffer  bytes.Buffer
}
		

Werte vom Typ SyncedBuffer sind sofort einsatzbereit, ob nach Speicherzuweisung oder Deklaration. Im folgenden Kodeschnipsel werden sowohl p als auch v ohne weiteres sofort funktionieren:

p := new(SyncedBuffer) // Typ *SyncedBuffer
var v SyncedBuffer     // Typ  SyncedBuffer
		

Konstruktoren und Verbundliterale

Manchmal ist ein Nullwert nicht gut genug, so dass ein Konstruktor mit Initialisierungen nötig wird, wie in diesem Beispiel aus dem Paket os:

func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
    f := new(File)
    f.fd = fd
    f.name = name
    f.dirinfo = nil
    f.nepipe = 0
    return f
}
		

Da drin gibt es eine Menge vorgestanzter Phrasen. Wir können es aber vereinfachen mit einem Verbundliteral; das ist ein Ausdruck, der bei jeder Auswertung eine neue Instanz erzeugt:

func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
    f := File{fd, name, nil, 0}
    return &f
}
		

Übrigens ist es — anders als in C — vollkommen in Ordnung, die Adresse einer lokalen Variablen zurückzugeben; der Speicherbereich der Variablen überlebt nämlich die Funktion. Und weil bereits mit dem Adressieren eines Verbundliteral eine neue Instanz erzeugt wird, können wir die beiden letzten Zeilen kombinieren:

    return &File{fd, name, nil, 0}
		

Die Felder eines Verbundliterals haben eine feste Reihenfolge und müssen alle vorhanden sein. Allerdings, wenn die Elemente mit einem Label etikettiert werden, geschrieben als Feld:Wert-Paare, ist die Reihenfolge egal und die nicht genannten behalten ihren Nullwert. Also könnten wir schreiben:

    return &File{fd: fd, name: name}
		

Ein Grenzfall ist das Verbundliteral ohne Felder; es erzeugt einen Nullwert des Typs. Die Ausdrücke new(File) und &File{} sind gleichwertig.

Verbundliterale gibt es auch für Arrays, Slices und Maps, wobei die Label jeweils passende Indices oder Schlüssel sein müssen. Die folgenden Beispiele funktionieren mit beliebigen Werten von Enone, Eio und Einval, solange diese sich nur voneinander unterscheiden:

a := [...]string   {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
s := []string      {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
m := map[int]string{Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
		

Speicherzuteilung mit make

Zurück zur Speicherzuteilung. Die eingebaute Funktion make(T, args) erfüllt einen anderen Zweck als new(T). Sie erzeugt ausschließlich Slices, Maps und Channels, und gibt einen initialisierten (nicht nullilisierten) Wert vom Typ T (nicht *T) zurück. Das ist deshalb so, weil diese drei Typen unter der Haube Referenzen sind, und zwar auf Datenstrukturen, die vor Gebrauch initialisiert werden müssen. Zum Beispiel ist ein Slice eine Beschreibung, die aus drei Teilen besteht: dem Zeiger auf die Daten (in einem Array), der Länge und der Kapazität; und solange die nicht initialisiert sind, ist das Slice nil. Für Slices, Maps und Channels also initialisiert make die interne Datenstruktur und bereitet die Werte für den Gebrauch vor.

make([]int, 10, 100)
		

reserviert zum Beispiel ein Array für 100 Ganzzahlen und erzeugt dann ein Slice mit Länge 10 und Kapazität 100 und zeigt auf das erste Element des Array. (Beim Erzeugen von Slices kann man die Kapazität weglassen — genaueres dazu im Abschnitt über Slices.) Anders new([]int): dieses gibt einen Zeiger auf eine neu erzeugte Slice-Struktur mit Nullwerten zurück, d.h. einen Zeiger auf einen nil-Wert.

Folgende Beispiele illustrieren den Unterschied zwischen new und make:

var p *[]int = new([]int)       // erzeugt eine Slice-Struktur; *p == nil; wenig sinnvoll
var v  []int = make([]int, 100) // das Slice v verweist auf ein neues Array für 100 Integer

// Unnötig kompliziert:
var p *[]int = new([]int)
*p = make([]int, 100, 100)

// Go-typisch:
v := make([]int, 100)
		

Aber nicht vergessen: make gilt nur für Maps, Slices und Channels, und es gibt keinen Zeiger zurück! Um explizit einen Zeiger zu erhalten, reservieren Sie mit new.

Arrays

Arrays sind hilfreich, wenn man das Speicherlayout im Einzelnen planen will, manchmal helfen sie, Speicherzuweisungen zu vermeiden, doch in erster Linie sind sie Bausteine für Slices, die im nächsten Abschnitt besprochen werden. Als Fundament dafür erstmal ein paar Worte zu Arrays.

Arrays in Go und C unterscheiden sich wesentlich. In Go

Die Wert-Eigenschaft kann nützlich sein, aber auch teuer. Wenn Sie Verhalten und Effizienz von C haben wollen, können Sie Zeiger auf Arrays weitergeben:

func Sum(a *[3]float64) (sum float64) {
    for _, v := range *a {
        sum += v
    }
    return
}

array := [...]float64{7.0, 8.5, 9.1}
x := Sum(&array) // Beachte den expliziten Adresse-von-Operator
		

Aber das ist kein typischer Go-Stil. Slices sind typisch!

Slices

Slices [deutsch: Abschnitt, Schnitte, A.d.Ü.] kapseln Arrays, um einen allgemeineren, leistungsfähigeren und bequemeren Zugang zu einer Datensequenz bereitzustellen. Mal abgesehen von solchen Sachen mit festgelegter Dimension, wie etwa Transformationsmatrizen, wird in Go eher mit Slices gearbeitet als mit einfachen Arrays.

Slices sind Referenztypen; das heißt, wenn Sie ein Slice einem anderen zuweisen, beziehen sich beide auf dasselbe darunterliegende Array. Wenn beispielsweise eine Funktion ein Slice als Argument übergeben bekommt, wird jede Änderung an einem Element auch für den Aufrufer sichtbar, genauso wie wenn ein Zeiger auf das Array übergeben worden wäre. Eine Read-Funktion tut sich aber leichter mit einem Slice als mit Zeiger und Zähler; die Länge des Slice legt die Obergrenze für die Länge der zu lesenden Daten bereits fest. Hier die Signatur der Read-Methode aus dem Paket os:

func (file *File) Read(buf []byte) (n int, err error)
		

Die Methode gibt die Anzahl der gelesenen Bytes und, wenn nötig, einen Fehlerwert zurück. Um in die ersten 32 Bytes eines größeren Puffers b zu lesen, schneiden [englisch: slice, A.d.Ü.] Sie den Puffer auf:

    n, err := f.Read(buf[0:32])

So ein "Slicing" ist üblich und effizient. Effizienz mal beiseite, würde auch das folgende Kodestück in die ersten 32 Byte des Puffers lesen:

    var n int
    var err error
    for i := 0; i < 32; i++ {
        nbytes, e := f.Read(buf[i:i+1]) // Lies ein Byte
        if nbytes == 0 || e != nil {
            err = e
            break
        }
        n += nbytes
    }
		

Die Länge eines Slice darf geändert werden, solange es noch in den Grenzen des darunterliegenden Array liegt; man kann dem Slice eine Scheibe von sich selbst zuweisen. Die Kapazität eines Slice — man kann sie mit der eingebauten Funktion cap abfragen — zeigt die maximale Länge an. Hier nun eine Funktion, die Daten an ein Slice anhängt; wenn die Daten die Kapazität überfordern, wird neuer Speicher angefordert und ein neues Slice zurückgegeben. Die Funktion vertraut darauf, dass len und cap auch für ein nil-Slice gültig sind; sie geben dann 0 zurück.

func Append(slice, data []byte) []byte {
    l := len(slice)
    if l + len(data) > cap(slice) { // neuer Speicher nötig
        // Reserviere doppelt soviel wie nötig - für weiteres Wachstum.
        newSlice := make([]byte, (l+len(data))*2)
        // Die copy-Funktion funktioniert mit jedem Slice-Typ.
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0:l+len(data)]
    for i, c := range data {
        slice[l+i] = c
    }
    return slice
}
		

Auch wenn Append nur die Elemente von slice verändert, müssen wir das Slice selbst am Ende wieder zurückgeben, weil es — also die Datenstruktur, welche Zeiger, Länge und Kapazität enthält — als Wert übergeben wurde.

Einem Slice etwas anzuhängen, ist so nützlich, dass wir diese Idee in der eingebauten Funktion append eingefangen haben. Es fehlen uns aber noch Informationen, um deren Arbeitsweise verstehen zu können — wir kommen deshalb später darauf zurück.

Maps

Maps sind zweckmäße und leistungsfähige eingebaute Datenstrukturen, die Werte verschiedenen Typs assoziieren. Schlüssel kann jeder Typ sein, für den der Gleichheitsoperator definiert ist, wie Ganzzahlen, Fließkommazahlen, komplexe Zahlen, Strings, Zeiger und Interfaces (solange deren zugehörigen Typen Gleichheit unterstützen). Struct, Arrays oder Slices können dagegen nicht als Map-Schlüssel dienen, weil für sie Gleichheit nicht definiert ist. Wie Slices sind Maps Referenztypen. Übergibt man eine Map an eine Funktion, die den Inhalt der Map ändert, so sind die Änderungen für den Rufer sichtbar.

Erzeugen kann man Maps mithilfe von Verbundliteralen mit Schlüssel-Wert-Paaren, die durch Doppelpunkt getrennt sind — einfach so:

var timeZone = map[string] int {
    "UTC":  0*60*60, // Coordinated Universal Time
    "EST": -5*60*60, // Eastern Standard Time
    "CST": -6*60*60, // Central Standard Time
    "MST": -7*60*60, // Mountain Standard Time
    "PST": -8*60*60, // Pacific Standard Time
}
		

Die Syntax des Zuweisens und Abgreifens von Map-Werten sieht genauso aus wie die von Arrays, nur das der Index keine Ganzzahl zu sein braucht.

offset := timeZone["EST"]
		

Der Versuch, mit einem nicht vorhandenen Schlüssel auf eine Map zuzugreifen, gibt den Nullwert des Wertetyps der Map zurück. Enthält die Map zum Beispiel Ganzzahlen, so liefert der Zugriff mit einem nicht existenten Schlüssel 0 zurück. Ein "Set" kann man als Map mit dem Wertetyp bool implementieren. Um einen Wert ins Set aufzunehmen, setzen Sie den Map-Eintrag auf true; Vorhandensein testen Sie dann einfach durch Zugriff mit dem Index:

teilgenommen := map[string] bool {
	"Anne":   true,
	"Johann": true,
	...
}

...
if teilgenommen[person] { // false, wenn Person nicht in der Map
	fmt.Println(person, "hat teilgenommen")
}
		

Manchmal müssen Sie zwischen einem fehlenden Eintrag und einem Nullwert unterscheiden können: "Gibt es den Eintrag "UTC" in der Map? Oder krieg' ich 0 zurück, weil es ihn nicht gibt?" Nun, das kann man unterscheiden mit einer Mehrfachzuweisung:

var seconds int
var ok bool
seconds, ok = timeZone[tz]

Es dürfte klar sein, warum man das die "Komma-Ok"-Schreibweise nennt. Wenn tz existiert, dann wird seconds entsprechend gefüllt, wenn nicht, dann wird seconds Null und ok wird false. Hier nun eine Funktion, die das mit einer netten Fehlermeldung kombiniert:

func offset(tz string) int {
    if seconds, ok := timeZone[tz]; ok {
        return seconds
    }
    log.Println("unbekannte Zeitzone:", tz)
    return 0
}
		

Um nur das Vorhandensein zu prüfen ohne Interesse am tatsächlichen Wert, benutzt man den Leeren Bezeichner, einen einfachen Unterstrich (_). Jeder Wert jedes Typs kann dem Leeren Bezeichner zugewiesen werden, oder er kann dafür deklariert werden — der Wert wird rückstandsfrei entsorgt. Wenn Sie also nur die Präsenz in der Map feststellen wollen, nehmen Sie den Leeren Bezeichner anstelle einer Variablen:

_, present := timeZone[tz]
		

Um einen Map-Eintrag zu löschen, benutzen Sie die eingebaute Funktion delete mit der Map und dem Schlüssel als Argumenten. Das funktioniert auch dann sicher, wenn der Eintrag gar nicht existiert.

delete(timeZone, "PDT") // (Pacific Daylight Time = Sommerzeit) zurück zur Standardzeit
		

Drucken

Formatiertes Drucken in Go ist im Stil ähnlich zur printf-Familie in C, nur allgemeiner und reichhaltiger. Die Funktionen sind im fmt-Paket zuhause und werden großgeschrieben: fmt.Printf, fmt.Fprintf, fmt.Sprintf und so weiter. Die String-Funktionen (Sprintf usw.) geben einen String zurück anstatt einen vorgegebenen Puffer zu füllen.

Es ist aber gar nicht nötig, mit einen Formatstring zu arbeiten. Zu jedem Printf, Fprintf oder Sprintf gibt es zwei weitere Funktionen, also z.B. Print und Println. Diese erwarten keinen Formatstring, sondern benutzen für jedes Argument ein festgelegtes Format. Die Println-Versionen fügen jeweils einen Zwischenraum zwischen die Argumente ein und hängen einen Zeilenvorschub an, während die Print-Versionen Zwischenräume nur einfügen, wenn die Operatoren rechts und links keine Strings sind. Folgende Kodezeilen erzeugen alle diegleiche Ausgabe:

fmt.Printf("Hallo %d\n", 23)
fmt.Fprint(os.Stdout, "Hallo ", 23, "\n")
fmt.Println("Hallo ", 23)
fmt.Println(fmt.Sprint("Hallo ", 23))
		

Wie schon in der Programmieranleitung erwähnt, akzeptieren fmt.Fprint & Co. als erstes Argument jedes Objekt, welches das io.Writer-Interface implementiert; die Variablen os.Stdout und os.Stderr sollten Ihnen vertraut sein.

Ab hier entfernen wir uns von C. Zu allererst akzeptieren die numerischen Formate wie %d schon mal keine Vorzeichen- oder Größenangaben. Stattdessen entscheidet die Druckroutine selbst anhand des Argumenttyps.

var x uint64 = 1<<64 - 1
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))
		

druckt

18446744073709551615 ffffffffffffffff; -1 -1
		

Wenn Ihnen die vorgegebene Umformung — z.B. Dezimaldarstellung für Integer — genügt, nutzen Sie das Sammelformat %v (v steht für "value"); das Ergebnis ist dasselbe, das auch Print oder Println liefern würden. Darüberhinaus kann dieses Format jeden Wert drucken, sogar Arrays, Structs und Maps. Hier ein Beispiel für die Zeitzonen-Map aus dem vorigen Abschnitt:

fmt.Printf("%v\n", timeZone)  // oder nur: fmt.Println(timeZone)
		

Das ergibt folgendes:

map[CST:-21600 PST:-28800 EST:-18000 UTC:0 MST:-25200]
		

Bei Maps werden die Schlüssel natürlich unsortiert ausgegeben. Beim Drucken eines Struct kommentiert das modifizierte Format %+v die Felder einer Struktur mit ihren Namen, und für alle Werte gilt: das modifizierte Format %#v druckt den Wert in vollständiger Go-Syntax:

type T struct {
    a int
    b float64
    c string
}
t := &T{ 7, -2.35, "abc\tdef" }
fmt.Printf("%v\n", t)
fmt.Printf("%+v\n", t)
fmt.Printf("%#v\n", t)
fmt.Printf("%#v\n", timeZone)
		

druckt:

&{7 -2.35 abc   def}
&{a:7 b:-2.35 c:abc     def}
&main.T{a:7, b:-2.35, c:"abc\tdef"}
map[string] int{"CST":-21600, "PST":-28800, "EST":-18000, "UTC":0, "MST":-25200}
		

(Man beachte die "&".) Strings in Anführungszeichen erhält man auch, wenn %q auf Strings oder Byte-Slices ([]byte) angewendet wird; das alternative Format %#q benutzt stattdessen, wenn möglich, Gravis-Zeichen. %x funktioniert mit Strings und Byte-Arrays genauso wie mit Ganzzahlen und erzeugt einen langen Hexadezimal-String; mit einem Leerzeichen im Format (% x) werden die Bytes durch Leerzeichen getrennt dargestellt.

Praktisch ist auch %T, welches den Typ zu einem Wert ausdruckt.

fmt.Printf("%T\n", timeZone)
		

druckt:

map[string] int
		

Wenn Sie für einen selbstgeschneiderten Typ das Vorgabeformat festlegen wollen, haben Sie nur eine Methode mit der Signatur String() string für diesen Typ zu definieren. Das sieht dann für unseren Typ T vielleicht so aus:

func (t *T) String() string {
    return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}
fmt.Printf("%v\n", t)
		

Damit druckt man folgendes:

7/-2.35/"abc\tdef"
		

(Wenn Sie sowohl Werte vom Typ T als auch Zeiger auf T zu drucken haben, muss der Empfängertyp für die String-Methode ein Wert sein; im Beispiel oben haben wir aber einen Zeiger benutzt, weil das effizienter ist, und außerdem das für Strukturen Übliche. Mehr dazu weiter unten im Abschnitt Zeiger vs. Werte.)

Unsere String-Methode kann Sprintf rufen, weil die Druckfunktionen eintrittsinvariant [englisch: reentrant, A.d.Ü.] sind, also auch rekursiv nutzbar. Wir können sogar noch einen Schritt weiter gehen und die Argumente einer Druckroutine direkt an eine andere weitergeben. Die Signatur von Printf benutzt den Typ ...interface{} für seine letzten Argumente, um anzuzeigen, dass beliebig viele Parameter (beliebigen Typs) folgen können:

func Printf(format string, v ...interface{}) (n int, errno error) {
		

Innerhalb der Funktion Printf agiert v als Variable vom Typ []interface{}, wird aber in Form eine Argumentliste an eine andere variadische Funktion weitergereicht. Hier die Implementierung der weiter oben benutzten Funktion log.Println. Sie gibt ihre Argumente fürs Formatieren direkt an fmt.Sprintln weiter:

// Println arbeitet wie fmt.Println und schreibt ins Standard-Log.
func Println(v ...interface{}) {
    std.Output(2, fmt.Sprintln(v...)) // Output erwartet die Parameter (int, string)
}
		

Die drei Punkte ... nach v im Aufruf von Sprintln sagen dem Compiler, dass v eine Argumentliste ist; ohne würde v als ein Slice weitergegeben.

Zum Drucken gäbe es noch mehr zu sagen. Werfen Sie doch einen Blick in die godoc-Dokumentation des Pakets fmt.

Übrigens kann ein ...-Parameter auch einen festen Typ haben; zum Beispiel ...int für eine Minimum-Funktion, die die kleinste aus einer Liste von Ganzzahlen auswählt:

func Min(a ...int) int {
    min := int(^uint(0) >> 1) // größtmögliches int
    for _, i := range a {
        if i < min {
            min = i
        }
    }
    return min
}
		

Append

So, jetzt haben wir alle Informationen, um die eingebaute Funktion append erklären zu können. Die Signatur unterscheidet sich von der der obigen, selbstgestrickten Funktion Append, und sieht so aus:

func append(slice []T, elements...T) []T
		

, wobei T für einen beliebigen Typ steht. In Go kann man keine Funktion schreiben, bei der der Typ T erst vom Aufrufer festgelegt wird. Deshalb ist append "built-in" — es braucht Hilfe vom Compiler.

append hängt Elemente ans Ende des Slice an und gibt das Ergebnis zurück. Genau wie in unserem sebstgestrickten Append muss das Ergebnis-Slice zurückgegeben werden, weil das darunterliegende Array gewechselt haben kann. Das folgende einfache Beispiel:

x := []int{1,2,3}
x = append(x, 4, 5, 6)
fmt.Println(x)
		

druckt [1 2 3 4 5 6]. Also sammelt append eine beliebige Anzahl Argumente ein, ähnlich wie Printf.

Aber was ist, wenn wir das tun wollen, was unser eigenes Append getan hat, nämlich Slice an Slice zu hängen. Ruhig Blut, auch das ist einfach: wir benutzen ... beim Aufruf, genau wie oben beim Aufruf von Sprintln. Der folgende Kode produziert dieselbe Ausgabe wie der obige:

x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)
		

Ohne die drei Punkte würde die Umwandlung scheitern — die Typen würden nicht stimmen, das y wäre kein int.

Initialisierung

Auch wenn's oberflächlich nicht viel anders als in C oder C++ aussieht, bietet das Initialisieren in Go mehr. Während der Initialisierung können komplexe Strukturen erzeugt werden. Und die Reihenfolge beim Initialisieren von Objekten verschiedener Pakete wird korrekt gehandhabt.

Konstanten

Konstanten in Go sind, wie der Name schon sagt, konstant. Sie werden schon beim Kompilieren erzeugt, selbst wenn sie lokal in einer Funktion definiert sind, und sie sind Zahlen, Strings oder Bool-Werte — und zwar ausschließlich. Wegen der Festlegung auf den Übersetzungszeitpunkt muss der Ausdruck, der sie definiert, ein konstanter Ausdruck sein, d.h. für den Compiler auswertbar. Zum Beispiel ist 1<<3 ein konstanter Ausdruck, math.Sin(math.Pi/4) aber nicht, weil die Funktion math.Sin zur Laufzeit gerufen wird.

In Go werden Aufzählkonstanten mit dem iota-Zähler erzeugt. Da iota Teil eines Ausdrucks sein darf, und diese Ausdrücke implizit wiederholt werden können, ist es einfach, komplizierte Wertereihen zu bilden:

type ByteSize float64
const (
    _           = iota  // mit dem leeren Bezeichner den ersten Wert ignorieren
    KB ByteSize = 1<<(10*iota)
    MB
    GB
    TB
    PB
    EB
    ZB
    YB
)
		

Und weil man eine Funktion, also hier String, an einen Typ binden kann, ist es einem solchen Typ möglich, sich selbst fürs Drucken zu formatieren, sogart als Teil eines allgemeineren Typs:

func (b ByteSize) String() string {
    switch {
    case b >= YB:
        return fmt.Sprintf("%.2fYB", float64(b/YB))
    case b >= ZB:
        return fmt.Sprintf("%.2fZB", float64(b/ZB))
    case b >= EB:
        return fmt.Sprintf("%.2fEB", float64(b/EB))
    case b >= PB:
        return fmt.Sprintf("%.2fPB", float64(b/PB))
    case b >= TB:
        return fmt.Sprintf("%.2fTB", float64(b/TB))
    case b >= GB:
        return fmt.Sprintf("%.2fGB", float64(b/GB))
    case b >= MB:
        return fmt.Sprintf("%.2fMB", float64(b/MB))
    case b >= KB:
        return fmt.Sprintf("%.2fKB", float64(b/KB))
    }
    return fmt.Sprintf("%.2fB", float64(b))
}
		

(Die Konvertierungen mit float64 verhindern, dass Sprintf über die String-Methode von ByteSize rekursiv aufgerufen wird.) Der Ausdruck YB wird als 1.00YB gedruckt, und ByteSize(1e13) ergibt 9.09TB.

Variablen

Variablen können genau wie Konstanten initialisiert werden, nur dass ein beliebiger Ausdruck benutzt werden kann, der zur Laufzeit berechnet wird.

var (
    HOME = os.Getenv("HOME")
    USER = os.Getenv("USER")
    GOROOT = os.Getenv("GOROOT")
)
		

Die init-Funktion

Zu guter Letzt kann jede Quelldatei ihre eigene argumentlose init-Funktion definieren, um anzulegen, was auch immer anzulegen ist. (Zu jede Datei kann es sogar mehrere init-Funktionen geben.) Die einzige Einschränkung ist, dass Goroutinen, die während der Initialisierung losgeschickt werden, erst nach deren Ende anlaufen; die Initialisierung läuft nämlich immer als alleiniger Thread. "Zu guter Letzt" ist wörtlich gemeint; init wird erst gerufen, wenn für alle Variablendeklarationen des Pakets die Initialisierer ausgewertet sind, und die werden erst ausgewertet, wenn alle importierten Pakete initialisiert sind.

Neben dem Initialisieren selbst, wenn es nicht in einer Deklaration ausgedrückt werden kann, wird in init-Funktionen üblicherweise die Korrektheit des Programmumfelds geprüft oder korrigiert, bevor die echte Verarbeitung beginnt:

func init() {
    if USER == "" {
        log.Fatal("$USER nicht gesetzt")
    }
    if HOME == "" {
        HOME = "/usr/" + USER
    }
    if GOROOT == "" {
        GOROOT = HOME + "/go"
    }
    // GOROOT kann durch den Parameter --goroot auf der Kommandozeile überschrieben werden.
    flag.StringVar(&GOROOT, "goroot", GOROOT, "Go root directory")
}
	

Methoden

Zeiger vs. Werte

Methoden können für jeden benannten Typ definiert werden, nur nicht für Zeiger oder Interfaces; der Empfängertyp braucht kein Struct zu sein.

Im Abschnitt über Slices weiter oben haben wir eine Funktion Append geschrieben Wir können sie auch als Methode für Slices definieren. Dafür deklarieren wir zuerst einen benannten Typ mit dem wir die Methode verknüpfen können, und nehmen dann als Empfängerobjekt der Methode einen Wert dieses Typs:

type ByteSlice []byte

func (slice ByteSlice) Append(data []byte) []byte {
    // Kode genauso wie oben
}
		

Das erfordert immer noch die Rückgabe des veränderten Slice. Weniger unbeholfen wird es, wenn wir die Methode umdefinieren, so dass der Zeiger auf ein ByteSlice zum Empfängerobjekt wird und somit die Methode das Slice des Rufers überschreiben kann:

func (p *ByteSlice) Append(data []byte) {
    slice := *p
    // Kode genauso wie oben, nur ohne return
    *p = slice
}
		

Und es geht noch besser. Wenn wir unsere Methode so abändern, dass sie wie eine Standard-Write-Methode aussieht, nämlich so:

func (p *ByteSlice) Write(data []byte) (n int, err error) {
    slice := *p
    // Wieder wie oben
    *p = slice
    return len(data), nil
}
		

dann befriedigt der Typ *ByteSlice sogar das Standard-Interface io.Writer — und das ist praktisch. Wir können dann zum Beispiel reindrucken:

    var b ByteSlice
    fmt.Fprintf(&b, "This hour has %d days\n", 7)
		

Wir übergeben die Adresse von ByteSlice, weil nur *ByteSlice den io.Writer befriedigt. Die Regel zu "Zeiger vs. Werte" lautet: Methoden, die Werte zurückgeben, können sowohl an Zeiger als auch an Werte gebunden werden, Methoden, die Zeiger zurückgeben, nur an Zeiger. Zeiger-Methoden können das Empfängerobjekt verändern; verändert man hingegen nur die Kopie eines Wertes, so geht die Änderung verloren.

Übrigens ist die Idee, Write auf ein Slice anzuwenden, in bytes.Buffer implementiert.

Interfaces und andere Typen

Interfaces

Mit Interfaces in Go ["Rolle" wäre eine treffende Übersetzung, A.d.Ü.] bestimmt man das Verhalten eines Objekts: Wenn ein Objekt so etwas tun kann, dann kann man es auch hier benutzen. Wir haben einfache Beispiele schon kennengelernt; angepasste Druckfunktionen kann man mit einer String-Methode realisieren, und Fprintf kann auf jedes Objekt mit einer Write-Methode schreiben. Interfaces mit nur einer oder zwei Methoden sind typisch für Go; üblich ist auch, ihnen einen von der Methode abgeleiteten Namen zu geben, wie io.Writer für etwas, das Write implementiert.

Ein Typ kann vielen Interfaces genügen. Eine Sammlung von Objekten [englisch: collection, A.d.Ü.] etwa kann mit den Routinen des Pakets sort sortiert werden, wenn sie das sort.Interface implementiert, das heißt die Methoden mit den Signaturen Len(), Less(i, j int)bool und Swap(i, j int), und sie kann außerdem noch einen Formatierer haben. In dem folgenden konstruierten Beispiel erfüllt Sequence beides.

type Sequence []int

// Methoden, die das sort.Interface braucht.
func (s Sequence) Len() int {
    return len(s)
}
func (s Sequence) Less(i, j int) bool {
    return s[i] < s[j]
}
func (s Sequence) Swap(i, j int) {
    s[i], s[j] = s[j], s[i]
}

// Methode fürs Drucken - sortiert die Elemente vor dem Drucken.
func (s Sequence) String() string {
    sort.Sort(s)
    str := "["
    for i, elem := range s {
        if i > 0 {
            str += " "
        }
        str += fmt.Sprint(elem)
    }
    return str + "]"
}
		

Konvertierung

Die String-Methode von Sequence rekonstruiert, was Sprint bereits für Slices tut. Von dessen Arbeit können wir profitieren, wenn wir Sequence nach []int konvertieren, bevor wir Sprint rufen.

func (s Sequence) String() string {
    sort.Sort(s)
    return fmt.Sprint([]int(s))
}

Diese Konvertierung bewirkt, dass s wie ein normales Slice behandelt wird, also wie vorgegeben formatiert wird. Ohne die Konvertierung würde Sprint die String-Methode von Sequence finden und sich endlos wiederholen. Die Konvertierung ist erlaubt, weil die beiden Typen (Sequence und []int) gleich sind, wenn wir mal die Typnamen ignorieren; sie erzeugt keinen neuen Wert, sie tut nur so, als ob der vorhandene Wert von einem anderen Typ sei. (Es gibt aber andere erlaubte Konvertierungen, von Ganzzahl zu Fließkommazahl zu Beispiel, die einen neuen Wert erzeugen.)

Es ist typischer Go-Stil, Typen zu konvertieren, um Zugang zu weiteren Methoden zu bekommen. Ein Beispiel dafür wäre, den vorhandenen Typ sort.IntSlice zu nutzen, um das gesamte Sequence-Beispiel auf das hier zu reduzieren:

type Sequence []int

// Methode fürs Drucken - sortiert die Elemente vor dem Drucken.
func (s Sequence) String() string {
    sort.IntSlice(s).Sort()
    return fmt.Sprint([]int(s))
}
		

Anstatt dass Sequence mehrere Interfaces implementiert (sortieren und drucken), nutzen wir jetzt die Fähigkeit von Datenobjekten, in verschiedene Typen konvertiert werden zu können (Sequence, sort.IntSlice und []int), deren jeder einen Teil der Arbeit erledigt. Das ist in der Praxis vielleicht ungewohnt, kann aber sehr effektiv sein.

Allgemeingültigkeit

Wenn ein Typ nur dazu da ist, ein Interface zu implementieren, und er darüberhinaus keine weitere Methode exportiert, braucht auch der Typ selbst nicht exportiert zu werden. Nur das Interface zu exportieren macht klar, dass das Verhalten entscheidend ist, nicht die Implementierung, und dass auch andere Implementierungen mit anderen Eigenschaften das Verhalten des ursprünglichen Typs wiederspiegeln können. Man erspart es sich auch, eine gemeinsame Methode für jede Implementierung extra dokumentieren zu müssen.

In solchen Fällen sollte ein Konstruktor einen Interface-Wert zurückliefern, und nicht den Typ, der es implementiert. Zum Beispiel liefern in den Hash-Bibliotheken sowohl crc32.NewIEEE als auch adler32.New den Interface-Typ hash.Hash32 zurück. Um in einem Go-Programm den CRC-32- durch den Adler-32-Algorithmus zu ersetzen, hat man nur den Konstruktor-Aufruf zu ändern, der Rest des Kodes bleibt unberührt.

Ein ähnliches Vorgehen erlaubt es, die Verschlüsselung für Datenströme in den diversen crypto-Paketen von denen für Blöcke, aus denen sie zusammengesetzt sind, getrennt zu halten. Das Block-Interface im Paket crypto/cipher legt das Verhalten eines Block-Chiffrierers fest, welcher das Schlüsseln eines einzelnen Datenblocks bereitstellt. Analog zum bufio-Paket kann ein Chiffrier-Paket, das dieses Interface implementiert, benutzt werden, um einen Chiffrierer für Datenströme zu bauen; vertreten durch das Stream-Interface und zwar ohne Detailwissen über die Block-Verschlüsselung.

Das crypto/cipher-Interface sieht so aus:

type Block interface {
    BlockSize() int
    Encrypt(src, dst []byte)
    Decrypt(src, dst []byte)
}

type Stream interface {
    XORKeyStream(src, dst []byte)
}
		

Hier ist die Definition eines Zähler-Modus-Stroms (CTR), welcher aus einem Block-Chiffrierer einen Strom-Chiffrierer macht; die Details des Block-Chiffrierers werden wegabstrahiert:

// NewCTR gibt einen Strom zurück, der ver- und entschlüsselt,
// indem er den gegebenen Block im Zähler-Modus benutzt.
// Die Länge von iv muss der Blocklänge von block entsprechen.
func NewCTR(block Block, iv []byte) Stream
		

NewCTR beziehen sich nicht allein auf einen bestimmeten Verschlüsselungs-Algorithmus und eine beistimmte Datenquelle, sondern auf jede Implementierung des Block-Interface und jeden Stream. Und weil sie beide Interface-Werte zurückgeben, ist das Ersetzen der CTR-Verschlüsselung durch eine andere nur eine kleine Änderung: der Aufruf des Konstruktors muss bearbeitet werden, und da der umliegende Kode das Ergebnis nur als Stream behandeln darf, wird er davon gar nichts mitkriegen.

Interfaces und Methoden

Weil man an fast alles Methoden anknüpfen kann, kann auch fast alles einem Interface genügen. Ein schönes Beispiel ist das Paket http, welches das Handler-Interface definiert. Jedes Objekt, das Handler implementiert kann HTTP-Anfragen bedienen.

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}
		

ResponseWriter ist selbst wieder ein Interface, das all die Methoden bereitstellt, die nötig sind, um dem Klienten die Antwort zu schicken. Dazu gehört die Standard-Write-Methode, so dass man http.ResponseWriter überall dort einsetzen kann, wo auch io.Writer funktioniert.

Um es einfacher zu machen, ignorieren wir die POST-Methode und tun so als ob eine HTTP-Anfrage immer ein GET wäre; die Vereinfachung ändert nichts an der Art, wie diese Behandler gebaut werden. Hier eine einfache aber vollständige Implementierung eines Behandlers zum Zählen der Besucher einer Seite:

// Ein einfacher Zähl-Server
type Counter struct {
    n int
}

func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    ctr.n++
    fmt.Fprintf(w, "Zähler = %d\n", ctr.n)
}
		

(Mit unserem Hauptthema im Blick, beachten Sie, wie Fprintf in ein http.ResponseWriter schreiben kann!) Und so wird ein solcher Server mit dem Knoten eines URL-Baums verknüpft:

import "net/http"
...
ctr := new(Counter)
http.Handle("/counter", ctr)
		

Aber warum ist Counter ein Struct? Eine Ganzzahl ist doch alles, was wir brauchen. (Das Empfängerobjekt muss Zeiger sein, damit das Hochzählen vom Rufer gesehen werden kann.)

// Ein noch einfacherer Zähl-Server
type Counter int

func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    *ctr++
    fmt.Fprintf(w, "Zähler = %d\n", *ctr)
}
		

Was tun, wenn Ihr Programm verständigt werden muss, sobald eine Seite besucht wird? Nun, verknüpfen Sie die Seite mit einem Kanal!

// Ein Channel, der bei jedem Besuch eine Nachricht sendet.
// (wahrscheinlich gepuffert)
type Chan chan *http.Request

func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    ch <- req
    fmt.Fprint(w, "Nachricht gesendet")
}
		

Nehmen wir schließlich mal an, wir wollen in /args festhalten, mit welchen Argumenten das Serverprogramm gestartet wurde. Die Funktion, die die Argumente druckt, ist einfach:

func ArgServer() {
    for _, s := range os.Args {
        fmt.Println(s)
    }
}
		

Wie machen wir daraus einen HTTP-Server? Nun, wir könnten irgendeine ArgServer-Methode bauen, deren Wert wir ignorieren, aber es gibt eine sauberere Lösung. Da wir ja für jeden Typ — außer Zeiger oder Interface — Methoden definieren können, können wir das auch für eine Funktion. Das Paket http enthält folgenden Kode:

// Der Typ HandlerFunc ist ein Adapter, mit dem normale Funktionen
// als HTTP-Behandler benutzt werden.  Wenn f eine Function
// mit der passenden Signatur ist, dann ist HandlerFunc(f) ein
// Behandler-Object, das f ruft.
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP ruft f(c, req).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
    f(w, req)
}
		

HandlerFunc ist ein Typ mit einer Methode ServeHTTP. Also können Werte dieses Typs HTTP-Anfragen bedienen. Betrachten Sie die Implementierung der Methode: das Empfängerobjekt ist eine Funktion f, und die Methode ruft f auf. Das schaut vielleicht seltsam aus, ist aber nicht viel anderes als, sagen wir mal, ein Channel als Empfängerobjekt und eine Methode, die in den Kanal sendet.

Um ArgServer zu einem HTTP-Server zu machen, ändern wir so, dass die Signatur stimmt:

// Argument-Server.
func ArgServer(w http.ResponseWriter, req *http.Request) {
    for _, s := range os.Args {
        fmt.Fprintln(w, s)
    }
}
		

ArgServer hat jetzt die gleiche Signatur wie HandlerFunc, also kann es in diesen Typ konvertiert werden, um auf dessen Methoden zuzugreifen — genauso wie wir Sequence zu IntSlice konvertiert haben, um auf IntSlice.Sort zuzugreifen. Der aufrufende Kode ist kurz:

http.Handle("/args", http.HandlerFunc(ArgServer))
		

Wenn jemand die Seite /args besucht, so hat der Behandler dieser Seite der Wert ArgServer und den Typ HandlerFunc. Der HTTP-Server wird die Methode ServeHTTP für diesen Typ aufrufen mit ArgServer als Empfängerobjekt. Dieses wiederum wird ArgServer rufen, indem es f(c, req) innerhalb von HandlerFunc.ServeHTTP ruft. Dann werden die Argumente angezeigt.

In diesem Abschnitt haben wir aus einem Struct, einer Ganzzahl, einem Kanal und einer Funktion je einen HTTP-Server gemacht, all das war möglich, weil ein Interface einfach eine Sammlung von Methoden ist, die man für (fast) jeden Typ definieren kann.

Einbetten

Go kennt nicht den üblichen, typgeleiteten Begriff der Unterklasse, man kann aber Teile einer Implementierung "leihen", indem man Typen in eine Struktur oder ein Interface einbettet.

Einbetten in ein Interface geht ganz einfach. Wir haben die Interfaces io.Reader und io.Writer schon erwähnt; so sind sie definiert:

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

Das Paket io exportiert außerdem mehrere andere Interfaces, die Objekte beschreiben, welche mehrere dieser Methoden implementieren. Zum Beispiel ist io.ReadWriter ein Interface, welches sowohl die Read- als auch die Write-Methode enthält. Wir könnten io.ReadWriter definieren, indem wir die beiden Methoden explizit aufführten, aber viel einfacher ist es, die beiden Interfaces einzubetten, und so das neue zu formen. Also so:

// ReadWriter ist das Interface, das die grundlegenden Methoden
// Read und Write zusammenfasst.
type ReadWriter interface {
    Reader
    Writer
}
		

Das sagt genau das aus, was dort geschrieben steht: Ein ReadWriter kann tun, was ein Reader tut und was ein Writer tut; es ist die Vereinigung der eingebetteten Interfaces (deren Methodenmengen disjunkt sein müssen). In Interfaces kann man nur Interfaces einbetten.

Die gleiche Grundidee funktioniert auch für Strukturen, aber mit weiterreichenden Konsequenzen. Das bufio-Paket enthält zwei Struct-Typen, bufio.Reader und bufio.Writer, die natürlich beide die analogen Interfaces aus dem Paket io implementieren. Und bufio implementiert auch einen gepufferten Reader/Writer, indem es Reader und Writer durch Einbetten in eine Struktur kombiniert; es zählt die Typen innerhalb der Stuktur auf aber gibt ihnen keine Namen:

// ReadWriter enthält Zeiger auf einen Reader und einen Writer.
// Es implementiert io.ReadWriter.
type ReadWriter struct {
    *Reader  // *bufio.Reader
    *Writer  // *bufio.Writer
}
		

Die eingebetteten Strukturen sind Zeiger auf Strukturen und müssen natürlich, damit sie auf gültige Strukturen zeigen, vor Gebrauch initialisiert werden. Wir könnten die Struktur ReadWriter auch so schreiben:

type ReadWriter struct {
    reader *Reader
    writer *Writer
}
		

aber dann, um die Feldmethoden zu befördern und das io-Interface zu befriedigen, müssten wir auch Weiterleitungsmethoden wie die folgende vorsehen:

func (rw *ReadWriter) Read(p []byte) (n int, err error) {
    return rw.reader.Read(p)
}

Dadurch, dass wir die Structs direkt einbetten, vermeiden wir solcherlei Buchhalterarbeit. Die Methoden der eingebetteten Typen werden kostenlos mitgeliefert, und das bedeutet: bufio.ReadWriter besitzt nicht nur alle Methoden von bufio.Reader und bufio.Writer sondern genügt auch den drei Interfaces io.Reader, io.Writer und io.ReadWriter.

Es gibt einen wichtigen Unterschied zwischen Einbetten und Unterklassenbildung. Wenn wir einen Typ einbetten, werden die Methoden des eingebetteten Typs auch Methoden des äußeren Typs, doch wenn sie aufgerufen werden, ist das Empfängerobjekt der innere und nicht der äußere Typ. Wenn in unserem Beispiel die Read-Methode eines bufio.ReadWriter aufgerufen wird, hat das genau dengleichen Effekt, den die o.g. Weiterleitungsmethode hätte; Empfängerobjekt ist das reader-Feld von ReadWriter, nicht der ReadWriter selbst.

Einbetten kann bequem sein. Das folgende Beispiel zeigt ein eingebettetes neben einem regulären benannten Feld:

type Job struct {
    Command string
    *log.Logger
}
		

Der Typ Job besitzt jetzt auch Log, Logf und die anderen Methoden von *log.Logger. Natürlich hätten wir dem Logger auch einen Namen geben können, aber das ist nicht nötig. Und nun, nach einer Initialisierung, kann Job sogar protokollieren:

job.Log("los geht's...")
		

Der Logger ist ein reguläres Feld der Struktur, das wir, wie üblich, mit einen Konstruktor initialisieren können:

func NewJob(command string, logger *log.Logger) *Job {
    return &Job{command, logger}
}
		

oder mit einem Verbundliteral:

job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}
		

Wenn wir ein eingebettetes Feld direkt ansprechen müssen, dient der Typname ohne die Paketbezeichnung als Feldname. Wenn wir Zugriff brauchen auf den *log.Logger einer Variablen job vom Typ Job, dann schreiben wir job.Logger. Das ist hilfreich, um die Methoden von Logger zu verfeinern:

func (job *Job) Logf(format string, args ...interface{}) {
    job.Logger.Logf("%q: %s", job.Command, fmt.Sprintf(format, args))
}
		

Einbetten von Typen wirft das Problem von Namenskonflikten auf, doch die Regeln zum Auflösen sind einfach.

Erstens: Ein Feld oder eine Methode X verdeckt jedes andere Objekt X in einer tieferen Schicht des Typs: gäbe es im log.Logger ein Feld oder eine Methode namens Command, so würde das Command-Feld von Job dominieren.

Zweitens: Wenn der gleiche Name auf derselben Verschachtelungsebene wieder erscheint, ist das gewöhnlich ein Fehler; es wäre falsch den log.Logger einzubetten, wenn das Job-Struct auch noch ein Feld oder eine Methode Logger enthielte. Wie auch immer, wenn der doppelte Name außerhalb der Typdefinition nirgends im Programm genannt wird, dann ist das OK. Diese Einschränkung bietet einen gewissen Schutz gegen Änderungen eingebetteter Typen von außen; Es gibt kein Problem, wenn ein Feld hinzukommt, das mit einem anderen Feld in einem anderen Untertyp kollidiert ... wenn nur keines der Felder benutzt wird.

Nebenläufigkeit

"Share by communicating"

Nebenläufigkeit ist ein weites Feld, und hier haben wir nur Platz für die Go-typischen Glanzlichter.

Viele Umgebungen machen es uns schwer, Nebenläufigkeit zu programmieren, allein dadurch, dass der korrekte Zugriff auf gemeinsame Daten nur mit viel Scharfsinn zu implementieren ist. Go ermutigt eine andere Herangehensweise, bei der gemeinsame Daten über Kanäle herumgereicht werden und nie wirklich gleichzeitig von mehreren Verarbeitungssträngen benutzt werden. Zu jedem Zeitpunkt hat nur eine Goroutine Zugriff auf einen Wert. Konkurrenz um die Daten wird schon vom Design verhindert. Um diese Art Denken zu befördern, haben wir sie zu einem Motto eingedampft:

Do not communicate by sharing memory;
instead, share memory by communicating.
A.d.Ü.: Eine Übersetzungsübung für den Leser; meine eigenen Versuche waren wenig befriedigend. Hier das am wenigsten schlechte Ergebnis:
"Nicht reinreden, nacheinander reden!"

Natürlich kann man es auch übertreiben. Zum Beispiel geht Referenzzählen immer noch am besten mit einer Ganzzahl in einem Mutex. Aber als übergeodneter Ansatz macht die Zugriffkontrolle mithilfe der Channels es einfacher, klare und korrekte Programme zu schreiben.

Man kann sich dieses Modell auch so klar machen. Denken Sie sich ein typisches Programm, das nur einen Verarbeitungsstrang kennt: es hat keinen Bedarf für Synchronisation. Starten Sie nun ein zweites solches Programm: auch das braucht keine Synchronisation. Jetzt sollen die beiden sich unterhalten. Wenn die Nachrichten selbst das Synchronisationsmittel sind, braucht man immer noch nichts darüber hinaus; Unix-Pipelines zum Beispiel genügen diesem Modell perfekt. Wenn auch die Wurzeln für Nebenläufigkeit in Go die "Communicating Sequential Processes" (CSP) von Hoare sind, kann man sie als eine Art verallgemeinerte, typsichere Unix-Pipelines sehen.

Goroutinen

Wir nennen sie Goroutinen, weil die existierenden Begriffe — Threads, Koroutinen, Prozesse usw. — falsche Nebenbedeutungen haben. Die Goroutine hat ein simples Leitbild: sie ist eine Funktion, die parallel zu anderen Goroutinen im selben Adressraum abläuft. Sie ist leichtgewichtig, weil sie kaum mehr als ein bisschen Stack-Speicher kostet. Stack-Speicher beginnt klein, ist deshalb billig, und wächst durch Anfordern (und Freigeben) von Heap-Speicher je nach Bedarf.

Goroutinen werden auf mehrere OS-Threads verteilt, so dass, wenn eine blockieren sollte, weil sie z.B. auf Input wartet, die anderen weiterlaufen. Ihre äußere Erscheinung verdeckt viel von der Komplexität der Thread-Erzeugung und -Verwaltung.

Rufen Sie eine Funktion oder Methode mit dem Schlüsselwort go auf, damit sie in einer neuen Goroutine läuft. Wenn Funktion oder Methode endet, endet auch die Goroutine — geräuschlos. (Der Effekt ist ähnlich dem des Suffix & in Unix, der Kommandos im Hintergrund laufen lässt.)

go list.Sort() // Führe list.Sort parallel aus; warte nicht aufs Ergebnis.
		

Für das Starten einer Goroutine kann ein Funktionsliteral praktisch sein:

func Announce(message string, delay int64) {
    go func() {
        time.Sleep(delay)
        fmt.Println(message)
    }() // Beachte die runden Klammern - die Funktion muss gerufen werden.
}
		

Funktionsliterale in Go sind Funktionsabschlüsse [enlisch: closure, A.d.Ü.]: die Implementierung stellt sicher, dass die angesprochenen Variablen solange überleben, wie sie benutzt werden.

Die Beispiele oben sind nicht besonders praktisch, weil die Funktionen (noch) keine Möglichkeit haben, ihr Ende bekanntzumachen. Dafür brauchen wir Kanäle...

Channels

Wie Maps so sind auch Channels Referenztypen und werden mit make erzeugt. Wird der optionale Ganzzahl-Parameter mitgegeben, so legt dieser die Puffergröße des Kanals fest. Vorgegeben ist die Null für einen ungepufferten, d.h. synchronen Kanal.

ci := make(chan int)           // ungepufferter Channel für Ganzzahlen
cj := make(chan int, 0)        // ungepufferter Channel für Ganzzahlen
cs := make(chan *os.File, 100) // gepufferter Channel für Dateizeiger
		

Channels kombinieren Kommunikation — die Übermittlung eines Wertes — mit Synchronisation — der Garantie, dass sich zwei Berechnungen (Goroutinen) in einem definierten Zustand befinden.

Mit Channels gibt es viele nette Programmiermuster. Hier ist eins davon. Im vorigen Abschnitt hatten wir eine Sortierung im Hintergrund gestartet. Ein Kanal erlaubt der initiierenden Goroutine zu warten, bis die Sortierung beendet ist.

c := make(chan int) // Erzeuge einen Kanal.

// Starte die Sortierung in einer Goroutine; 
// signalisiere das Ende in den Kanal.
go func() {
    list.Sort()
    c <- 1 // Sende ein Signal, egal welches. 
}()

doSomethingForAWhile()

<-c // Warte aufs Ende der Sortierung; ignoriere den übermittelten Wert.
		

Empfänger "blockieren", bis etwas zum Empfangen da ist. Sender blockieren, wenn der Kanal gepuffert ist, nur solange, bis der Wert in den Puffer kopiert wird; wenn der Puffer voll ist, bis ein Empfänger einen Wert entnommen hat.

Ein gepufferter Kanal kann wie eine Verkehrsampel benutzt werden, um den Durchfluss zu begrenzen. Im folgenden Beispiel werden Anfragen an handle übergeben, welches einen Wert in den Kanal schickt, die Anfrage bearbeitet und dann einen Wert aus dem Kanal entnimmt. Die Kapazität des Kanalpuffers begrenzt die Anzahl der gleichzeitigen Aufrufe von process.

var sem = make(chan int, MaxOutstanding)

func handle(r *Request) {
    sem <- 1   // Warte bis der Kanal wieder etwas aufnimmt.
    process(r) // Das kann lange dauern.
    <-sem      // Fertig; mach Platz für nächstes process.
}

func Serve(queue chan *Request) {
    for {
        req := <-queue
        go handle(req) // Hier nicht warten.
    }
}
		

Es folgt eine Implementierung der gleichen Idee, in welcher eine, jetzt festgelegte, Anzahl von handle-Goroutinen gestartet wird, die alle vom Anfrage-Kanal lesen. Die Anzahl der Goroutinen begrenzt die Anzahl der parallelen Aufrufe von process. Die Serve-Funktion hier nimmt außerdem einen Kanal entgegen, auf dem ihr signalisiert werden kann, dass sie aufhören soll; nach dem Starten der Goroutinen wartet sie am Kanal auf Empfang.

func handle(queue chan *Request) {
    for r := range queue {
        process(r)
    }
}

func Serve(clientRequests chan *clientRequests, quit chan bool) {
    // Starten der handles
    for i := 0; i < MaxOutstanding; i++ {
        go handle(clientRequests)
    }
    <-quit // Warte aufs Kommando zum Aufhören.
}
		

Kanäle von Kanälen

Eine der wichtigsten Eigenschaften von Go ist, dass Channel Werte "erster Klasse" sind, die erzeugt und herumgereicht werden können wie andere auch. Eine typische Anwendung dieser Eigenschaft ist das sichere, parallele Aufteilen eines Bündels [englisch: demultiplexing, A.d.Ü.]

Im Beispiel aus dem vorigen Abschnitt war handle ein idealisierter Behandler für eine Anfrage, ohne dass wir den behandelten Typ definiert hatten. Wenn dieser Typ nun einen Kanal fürs Antworten enthielte, so würde jeder Klient seinen eigenen Antwortkanal mitbringen. Hier nun eine vereinfachte Definition des Typs Request:

type Request struct {
    args        []int
    f           func([]int) int
    resultChan  chan int
}
		

Der Klient liefert mit seinem Anfrageobjekt eine Funktion, deren Argumente sowie einen Kanal für die Antwort.

func sum(a []int) (s int) {
    for _, v := range a {
        s += v
    }
    return
}

request := &Request{[]int{3, 4, 5}, sum, make(chan int)}
// Anfrage senden.
clientRequests <- request
// Auf die Antwort warten.
fmt.Printf("answer: %d\n", <-request.resultChan)
		

Serverseitig ist nur die behandelnde Funktion zu ändern:

func handle(queue chan *Request) {
    for req := range queue {
        req.resultChan <- req.f(req.args)
    }
}
		

Für ein realistisches System bleibt noch eine Menge zu tun, doch dieser Kode bildet den Rahmen für ein quotiertes, paralleles, nicht-blockierendes RPC-System ... und weit und breit kein Mutex in Sicht.

Parallelisieren

Eine weitere Anwendung dieser Ideen ist das Verteilen von Berechnungen auf mehrere CPU-Kerne. Wenn die Berechnung in unabhängige Teile gespalten werden kann, können diese parallelisiert werden, wobei jeder Teil über einen Kanal mitteilt, wenn er fertig ist.

Nehmen wir mal den Fall einer rechenintensiven Operation auf die Elemente eines Vektors, wobei die Operation auf jedes Element unabhängig ist, wie in diesem idealisierten Beispiel:

type Vector []float64

// Wende die Operation an auf v[i], v[i+1] ... bis v[n-1].
func (v Vector) DoSome(i, n int, u Vector, c chan int) {
    for ; i < n; i++ {
        v[i] += u.Op(v[i])
    }
    c <- 1 // Ende der Arbeit signalisieren
}
		

Wir schicken die Teile unabhängig voneinander in einer Schleife los, jeweils einen Teil für eine CPU. Sie dürfen in beliebiger Reihenfolge fertig werden; nachdem wir alle losgeschickt haben, leeren wir nur den Kanal und zählen die Endesignale:

const NCPU = 4 // Anzahl der CPU-Kerne

func (v Vector) DoAll(u Vector) {
    c := make(chan int, NCPU) // Puffer nicht nötig aber sinnvoll.
    for i := 0; i < NCPU; i++ {
        go v.DoSome(i*len(v)/NCPU, (i+1)*len(v)/NCPU, u, c)
    }
    // Channel leermachen.
    for i := 0; i < NCPU; i++ {
        <-c // Warten auf das Ende einer Goroutine
    }
    // Alle beendet!
}
		

Die derzeitige Implementierung von gc (6g, ...) parallelisiert diesen Kode nicht automatisch; sie sieht für Benutzerprozesse nur einen CPU-Kern vor. Zwar können eine unbestimmte Anzahl von Goroutinen auf das Ende von Systemaufrufen warten, aber die Vorgabe heißt: zu einem Zeitpunkt läuft nur ein Benutzerprozess. Das sollte eigentlich intelligenter gehen, und eines Tages wird es das auch, aber bis dahin müssen Sie dem Laufzeitsystem explizit sagen, wieviele Goroutinen gleichzeitig ausgeführt werden sollen. Das kann auf zweierlei Art geschehen. Entweder starten Sie ihren Job mit einer Umgebungsvariablen GOMAXPROCS, die die Anzahl der zu nutzenden CPU-Kerne angibt, oder Sie importieren das Paket runtime und rufen runtime.GOMAXPROCS(NCPU). Ein hilfreicher Wert hierfür könnte etwa runtime.NumCPU() sein; das ist die Anzahl der logischen CPUs des lokalen Rechners. Aber noch einmal: Wir erwarten, dass diese Anforderung überflüssig wird, wenn Aufgabenmanagement und Laufzeitsystem besser werden.

Ein Puffer mit Lecks

Mit den Mitteln des nebenläufigen Programmierens kann man sogar nicht-nebenläufige Konzepte einfacher ausdrücken. Hier haben wir aus einem RPC-Paket ein Beispiel extrahiert. Die Klient-Goroutine kreiselt und empfängt dabei Daten aus einer Quelle, vielleicht über ein Netzwerk. Um reservieren und freigeben von Puffer zu vermeiden, hält sie sich eine Liste der freien Puffer in Form eines gepufferten Kanals. Wenn der Kanal leer ist, wird ein neuer Puffer reserviert. Ist dann der Puffer bereit, wird er über serverChan zum Server geschickt.

var freeList = make(chan *Buffer, 100)
var serverChan = make(chan *Buffer)

func client() {
    for {
        var b *Buffer
        // Schnapp dir einen Puffer, wenn einer da ist; reserviere einen neuen, wenn nicht.
        select {
        case b = <-freeList:
            // Jou, hab einen, und das war's schon.
        default:
            // Keiner da, also mach ich einen neuen.
            b = new(Buffer)
        }
        load(b)         // Lies die nächste Nachricht vom Netz.
        serverChan <- b // Ab damit zum Server.
    }
}
		

Die Verarbeitungsschleife des Servers empfängt jeweils eine Nachricht, verarbeitet sie und gibt den Puffer an die "Frei-Liste" zurück.

func server() {
    for {
        b := <-serverChan // Warte auf Arbeit.
        process(b)
        // Recycle den Puffer, wenn Platz dafür ist.
        select {
        case freeList <- b:
            // Puffer zurück in die Frei-Liste, ok, das war's.
        default:
            // Frei-Liste ist voll, dann machen wir einfach weiter.
        }
    }
}
		

Der Klient versucht von freeList einen Puffer zu erhalten; ist keiner verfügbar, reserviert er selbst einen. Durch Senden nach freeList gibt der Server b zurück an die Liste, es sei denn, diese ist schon voll. In dem Fall wird der Puffer fallengelassen, bis der Müllsammler [englisch: garbage collector, A.d.Ü.] sich darum kümmert. (Der Kode in default-Klauseln von select-Anweisungen wird dann ausgeführt, wenn keine der case-Bedingungen zutrifft, mit der Folge, dass ein select niemals blockiert.) So konstruiert man mit nur wenigen Zeilen Kode eine "Frei-Liste" in der Art eines lecken Eimers, wobei man sich ganz auf den gepufferten Kanal sowie auf den Müllsammler fürs Aufräumen verlässt.

Fehlerbehandlung

Bibliotheksroutinen müssen dem Aufrufer oft irgendwie einen Fehler melden können. In Go machen es, wie bereits erwähnt, die Multiplen Rückgabewerte leicht, neben dem erwarteten Wert eine detaillierte Fehlermeldung zurückzugeben. Konvention ist, dass dieser Fehler vom Typ error ist — einem einfachen, integrierten Interface:

type error interface {
    Error() string
}
		

Dem Programmierer einer Bibliothek steht es frei, dieses Interface unter der Haube reichhaltiger auszustatten, um nicht nur den nackten Fehler, sondern auch ein bisschen drumherum anzubieten. Zum Beispiel gibt os.Open einen os.PathError zurück:

// PathError hält einen Fehler fest, sowie die Operation
// und die Datei, bei der er auftrat.
type PathError struct {
    Op string   // "open", "unlink" ...
    Path string // Die betreffende Datei.
    Err error   // RÜckgabewert der Systemroutine.
}

func (e *PathError) Error() string {
    return e.Op + " " + e.Path + ": " + e.Err.Error()
}
		

Die Error-Methode von PathError erzeugt z.B.:

open /etc/passwx: no such file or directory
		

So eine Fehlertext, die den Dateiname, die Operation und den aufgetretenen Systemfehler enthält, ist nützlich, auch wenn sie weit vom Aufruf entfernt gedruckt wird; das ist viel mehr Information als nur "no such file or directory".

Wenn irgend sinnvoll möglich, sollten Fehlertexte ihre Herkunft melden, indem sie etwa den Namen des Pakets voranstellen, in dem der Fehler aufgetreten ist. Im Paket "image" zum Beispiel lautet der Fehlertext, wenn das Entschlüsseln fehlschlägt, weil das Format nicht erkannt wurde: "image: unknown format".

Aufrufer, die Wert auf genaue Fehlerdetails legen, können mit einen Typ-Switch oder einer Typprüfung nach bestimmten Fehlern suchen oder Details extrahieren. Nach einem PathErrors könnte man etwa das interne Feld Err daraufhin untersuchen, ob der Fehler zu beheben ist:

for try := 0; try < 2; try++ {
    file, err = os.Open(filename)
    if err == nil {
        return
    }
    if e, ok := err.(*os.PathError); ok && e.Err == os.ENOSPC {
        deleteTempFiles() // Platz schaffen.
        continue
    }
    return
}
		
Die zweite if-Anweisung hier ist typischer Go-Kode. Das Ergebnis der Typprüfung err.(*os.PathError) wird mit der "Komma-ok"-Schreibweise abgefragt (wie schon bei Maps beschrieben). Wenn die Typprüfung fehlschlägt, ist ok = false und e = nil. Wenn sie erfolgreich ist, dann ist ok = true, und der Fehler vom Typ *os.PathError, ebenso e, welches wir dann nach weiteren Informationen untersuchen können.

Panic

Die übliche Methode, einem Aufrufer einen Fehler zu melden, ist die Rückgabe des zusätzlichen Wertes error. Die Go-typische Read-Methode ist allgemein bekannt: sie gibt einen Bytezähler und einen error zurück. Aber was, wenn der Fehler nicht behoben werden kann? Manchmal darf ein Programm dann nicht weiterlaufen.

Zu diesem Zweck gibt es die eingebaute Funktion panic, die einen Laufzeitfehler erzeugt, der das Programm stoppt. (Doch beachte auch den folgenden Abschnitt.) Die Funktion erhält ein Argument beliebigen Typs — oft einen String — der noch vor Programmende gedruckt wird. So wird auch angezeigt, wenn etwas ganz unmögliches passiert ist, zum Beispiel, wenn eine Endlosschleife verlassen wurde. Der Compiler erkennt ein panic am Ende einer Funktion und prüft dann nicht mehr das Vorhandensein einer return-Anweisung.

// Spielzeug-Implementierung der Kubikwurzel nach der Newton-Methode.
func CubeRoot(x float64) float64 {
    z := x/3 // Beliebiger Anfangswert
    for i := 0; i < 1e6; i++ {
        prevz := z
        z -= (z*z*z-x) / (3*z*z)
        if veryClose(z, prevz) {
            return z
        }
    }
    // Keine Konvergenz nach einer Million Schritten - da ist was faul.
    panic(fmt.Sprintf("CubeRoot(%g) konvergiert nicht", x))
}
		

Das ist nur ein Beispiel, denn echte Bibliotheksfunktionen sollten panic vermeiden. Wenn das Problem versteckt oder umgangen werden kann, dann ist es auch besser, weiterzumachen anstatt das ganze Programm mit runterzureißen. Ein mögliches Gegenbeispiel ist das Initialisieren: wenn die Bibliothek dort keinen funktionsfähigen Zustand erreicht, darf sie vernünftigerweise in Panik verfallen — sozusagen.

var user = os.Getenv("USER")

func init() {
    if user == "" {
        panic("kein Wert vorhanden für $USER")
    }
}
		

Recover

Wird panic gerufen — wenn auch nur implizit wegen eines Laufzeitfehlers, etwa einer Array-Indizierung außerhalb des erlaubten Bereichs oder einer gescheiterten Typprüfung — so endet auf der Stelle die Ausführung der aktuellen Funktion, und es beginnt die Abwicklung des Goroutinen-Stapels; alle zurückgestellten Funktionen werden abgearbeitet. Wenn die letzte Ebene des Stapels verarbeitet wurde, endet das Programm. Aber mit der eingebauten Funktion recover kann man die Kontrolle über die Goroutine zurückerhalten und die normale Verarbeitung weiterführen.

Ein Aufruf von recover stoppt die Abwicklung und gibt das Argument zurück, welches panic übergeben wurde. Weil während der Abwicklung einzig der Kode zurückgestellter Funktionen ausgeführt wird, ist recover auch nur in zurückgestellten Funktionen von Nutzen.

Eine sinnvolle Anwendung von recover ist es, gescheiterte Goroutinen innerhalb eines Servers stillzulegen, ohne dass andere laufende Goroutinen beendet würden.

func server(workChan <-chan *Work) {
    for work := range workChan {
        go safelyDo(work)
    }
}

func safelyDo(work *Work) {
    defer func() {
        if err := recover(); err != nil {
            log.Println("work failed:", err)
        }
    }()
    do(work)
}
		

Wenn in diesem Beispiel do(work) panisch wird, dann wird das Ereignis protokolliert, und die Goroutine endet sauber, ohne andere zu stören. Kein Anlass, in der zurückgestellten Routine mehr zu tun; recover aufrufen genügt.

Weil recover immer nil zurückliefert, wenn es nicht direkt von einer zurückgestellten Funktion gerufen wird, kann zurückgestellter Kode auch problemlos Bibliotheksroutinen rufen, die ihrerseits panic und recover benutzen. Beispielsweise könnte die zurückgestellte Funktion in safelyDo eine Protokollroutine vor dem recover aufrufen, und die würde unabhängig vom Panikstatus ausgeführt. Mit diesem Recovery-Mechanismus rettet man die Funktion do (und alles, was von ihr aufgerufen wird) aus jeder schlimmen Situation, ganz sauber, allein indem man panic sagt. Wir können das benutzen, um Fehlerbehandlung in komplexer Software einfacher zu machen. Werfen wir einen Blick auf einen vereinfachten Ausschnitt aus dem Paket regexp; dieses meldet Fehler beim Parsen, indem es panic mit einem lokalen Fehlertyp ruft. Hier die Definitionen von Error, der error-Methode und der Funktion Compile.

// Error ist der Typ des Parse-Fehlers; er genügt dem error-Interface.
type Error string
func (e Error) Error() string {
    return string(e)
}

// error ist eine Methode von *Regexp, die einen Parse-Fehler
// über panic mit einem Error meldet.
func (regexp *Regexp) error(err string) {
    panic(Error(err))
}

// Compile gibt die Repräsentation eines regulären Ausdrucks zurück.
func Compile(str string) (regexp *Regexp, err error) {
    regexp = new(Regexp)
    // doParse ruft panic bei einem Parse-Fehler.
    defer func() {
        if e := recover(); e != nil {
            regexp = nil    // Rückgabewert löschen.
            err = e.(Error) // Bewirkt erneutes panic, wenn's kein Parse-Fehler war.
        }
    }()
    return regexp.doParse(str), nil
}
		

Wenn doParse die Panik kriegt, setzt der Recovery-Block den Rückgabewert auf nil — zurückgestellte Funktionen können nämlich benannte Rückgabewerte ändern. Dann wird mit einer Zuweisung zu err geprüft, ob das Problem wirklich ein Parse-Fehler war, indem e auf den lokalen Typ Error geprüft wird. Wenn das nicht der Fall ist, schlägt die Typprüfung fehl, wodurch ein Laufzeitfehler verursacht wird, so dass der "Stack" weiter abgewickelt wird, als ob es keine Unterbrechung gegeben hätte. Diese Prüfung stellt sicher, dass durch unerwartete Ereignisse, wie "Index nicht erlaubt", die Funktion fehlschlägt, obwohl wir mit panic und recover benutzerinduzierte Fehler abfangen.

Auf diese Weise kann die Methode error ganz einfach Parse-Fehler melden, ohne dass man den Parse-Stack per Hand aufdröseln müsste.

Aber: so nützlich dieses Programmiermuster auch ist, sollte es nur innerhalb eines Pakets benutzt werden. Parse macht aus seinen internen panic-Aufrufen error-Werte; es zeigt seine Panik nicht. Eine gute Regel!

Nebenbei bemerkt ändert das Zurückverfallen in Panik, wenn der Fehler ein unerwarteter war, den Wert von Panik. Trotzdem werden beide Panikattacken im Unfallbericht verzeichnet, so dass die ursprüngliche Problemursache sichtbar bleibt. Damit ist dieses simple Programmiermuster für den Normalfall gut genug — es ist und bleibt schließlich ein Programmabsturz. Wollen Sie aber nur den ursprünglichen Wert zeigen, so schreiben Sie ein bisschen mehr Kode, filtern den unerwarteten Fehler heraus und rufen panic mit dem ursprünglichen Wert: diese Übung sei dem Leser überlassen.

Ein Webserver

Schließen wollen wir mit einem kompletten Go-Programm, einem Webserver. Nun, es ist eher ein Server für einen Webserver. Google bietet auf http://chart.apis.google.com einen Dienst an, der automatisch Daten zu Diagrammen und Graphen verarbeitet. Interaktiv ist er aber schwer zu benutzen, weil man die Daten als Auftrag in die URL packen muss. Das folgende Programm stellt für eine speziellen Art von Daten eine hübschere Schnittstelle zur Verfügung: ausgehend von einem kurzen Stück Text ruft es den Diagrammserver, der daraus QR-Kode macht, eine Schachtel-Matrix, die den Text kodiert. Diese Bild kann Ihre Handykamera aufnehmen und, nur als Beispiel, als URL interpretieren, womit Sie sich das mühsame Eintippen der URL auf der winzigen Handytastatur sparen.

Hier das komplette Programm; die Erklärung folgt danach:

package main

import (
    "flag"
    "log"
    "net/http"
    "text/template"
)

var addr = flag.String("addr", ":1718", "http service address") // Q=17, R=18

var templ = template.Must(template.New("qr").Parse(templateStr))

func main() {
    flag.Parse()
    http.Handle("/", http.HandlerFunc(QR))
    err := http.ListenAndServe(*addr, nil)
    if err != nil {
        log.Fatal("ListenAndServe:", err)
    }
}

func QR(w http.ResponseWriter, req *http.Request) {
    templ.Execute(w, req.FormValue("s"))
}


const templateStr = `
<html>
<head>
<title>QR Link Generator</title>
</head>
<body>
{{if .}}
<img src="http://chart.apis.google.com/chart?chs=300x300&cht=qr&choe=UTF-8&chl={{urlquery .}}" />
<br>
{{html .}}
<br>
<br>
{.end}
<form action="/" name=f method="GET"><input maxLength=1024 size=70
name=s value="" title="Text zum QR-Kodieren"><input type=submit
value="Show QR" name=qr>
</form>
</body>
</html>
`
		

Bis zu main sollte dem leicht zu folgen sein. Der einzige Parameter legt einen HTTP-Port für unseren Server fest. Richtig lustig wird's dann in der Variablen templ: Sie enthält eine HTML-Schablone, die vom Server benutzt wird, um die Seite anzuzeigen; mehr dazu gleich.

Die Funktion main durchsucht die Parameter, und bindet, mit dem oben erwähnten Mechanismus die Funktion QR an das Wurzelverzeichnis des Servers. Dann wird die Funktion http.ListenAndServe gerufen, um den Server zu starten; diese "blockiert" solange der Server läuft.

Alles, was QR tut, ist, die Anfrage entgegenzunehmen, die die Formulardaten enthält, und die Schablone auf die Daten im Formular s anzuwenden.

Das Paket template ist sehr leistungsfähig; unser Programm kratzt gerade mal an seinen Möglichkeiten. Im Kern überschreibt es ein Stück Text im laufenden Betrieb, indem es Elemente durch Daten ersetzt, die an templ.Execute übergeben wurden; in diesem Fall sind es Formulardaten. Im Schablonentext (templateStr) zeigen die Teile in doppelt-geschweiften Klammern zu verarbeitende Teile an. Das Kodestück von {{if .}} bis {{end}} wird nur dann durchlaufen, wenn der Wert des aktuellen Datenelements, welches . (Punkt) heißt, nicht leer ist. Mit anderen Worten: Ist der String leer, so wird dieser Teil der Schablone unterdrückt.

Der Schnipsel {{urlquery .}} besagt, dass die Daten mit der Funktion urlquery verarbeitet werden sollen, die den String für die Anzeige auf der Webseite sicher aufbereitet.

Der Rest der Schablone ist reines HTML zum Anzeigen. Sollte Ihnen diese Erklärung zu kurz sein, lesen Sie bitte die Dokumentation zum Paket template, wo all das gründlicher diskutiert wird.

Und schon ist er fertig: ein nützlicher Webserver aus nur ein paar Zeilen Kode plus etwas HTML-Text, der durch Daten modifiziert wird. Go ist leitungsfähig genug, um eine Menge in wenigen Zeilen passieren zu lassen.