Go Home Page
Die Programmiersprache Go

Effective Go — Deutsche Übersetzung

Das Original:
https://golang.org/doc/effective_go.html
Version of July 24, 2021
Diese Übersetzung:
https://bitloeffel.de/DOC/golang/effective_go_20210817_de.html
Stand: 04.08.2021
© 2010-21 Hans-Werner Heinzen @ Bitloeffel.de
Die Nutzung dieses Dokuments ist unter den Bedingungen der "Creative Commons Attribution 3.0"-Lizenz erlaubt. Für die verlinkten Quelldateien gelten andere Bestimmungen.
Für die Fachbegriffe gibt es hier noch eine Wörterliste.

Effektiv 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 geht über die Sprachbeschreibung (de), die Tour of Go (de) und über die Bedienungshinweise (de) hinaus, welche Sie vorher lesen sollten.

Beispiele

Die Go-Paketquellen sind nicht nur Kernbibliothek; sie sind auch Beispiele dafür, wie man diese Sprache benutzt. Darüberhinaus enthalten viele der Pakete in sich abgeschlossene Beispiele, die direkt von golang.org aus gestartet werden können, wie zum Beispiel dieses (Wenn nötig, klicken Sie auf das Wort "Example".) Wenn Sie Fragen haben zur Herangehensweise an ein Problem oder wie man etwas implementieren könnte, so liefern Dokumentation, Kode und Beispiele in der Bibliothek Antworten, 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 (auch über go fmt aufrufbar), welches statt auf Datei- auf Paketebene arbeitet, 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.

Beispielsweise 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 weniger davon als C oder Java. Kontrollanweisungen (if, for, switch) kommen ganz ohne aus. Außerdem ist die Rangreihenfolge der Operatoren (de), kürzer und klarer:
x<<8 + y<<16
bedeutet das, was hier die Leerzeichen andeuten, anders als in anderen Sprachen.

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 innerhalb eines Ausdrucks oder 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 Paketkommentar 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.

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 einfacher Text und werden nicht interpretiert. HTML- oder Markierungen wie _diese_ werden Zeichen für Zeichen wiedergegeben — also Finger weg auch davon. Allerdings macht godoc eine Anpassung, indem es nämlich eingerückten Text in einer Schriftart mit fester Schriftbreite anzeigt, passend für Kodeschnipsel. Der Paketkommentar des Pakets fmt erzielt damit schöne Effekte.

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. Hier ein Beispiel aus der Standardbibliothek:

// Compile parses a regular expression and returns, if successful,
// a Regexp that can be used to match against text.
func Compile(str string) (*Regexp, error) {

Wenn nämlich jeder Doc-Kommentar mit dem Namen des Kommentierten beginnt, dann können Sie das doc-Subkommando des go-Tools benutzen und die Ausgabe mit grep filtern. Sagen wir mal, Sie suchen nach der Durchsuchen-Funktion (parse) für reguläre Ausdrücke, können sich aber nicht an den Namen "Compile" erinnern; dann hilft Ihnen:

$ go doc -all regexp | grep -i parse

Würden hingegen alle Doc-Kommentare mit "This function..." beginnen, so würde Ihnen grep nicht helfen können. Weil aber jeder Doc-Kommentar in diesem Paket mit dem Namen beginnt, wird Ihre Erinnerung folgendermaßen wieder aufgefrischt:

$ go doc -all regexp | grep -i parse
    Compile parses a regular expression and returns, if successful, a Regexp
    MustCompile is like Compile but panics if the expression cannot be parsed.
    It simplifies safe initialization of global variables holding
$

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 ')'")
    ...
)

Das Gruppieren kann auch eine Beziehung zwischen Positionen anzeigen, wie zum Beispiel die Tatsache, 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. Sie sind sogar semantisch bedeutsam. 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/encoding/base64 wird als encoding/base64 importiert und hat als Namen base64, nicht encoding_base64 und auch nicht encodingBase64.

Der Importeur eines Pakets wird mit dem Namen auf dessen Inhalte bezugnehmen, so dass die vom Paket exportierten Namen auf Wiederholungen verzichten können. (Vermeiden Sie die import .-Schreibweise; diese ist nur vorgesehen, um Tests zu vereinfachen, die ja außerhalb des zu testenden Pakets ablaufen.) 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 man würde nichts gewinnen, wenn man once.DoOrWaitUntilDone(setup) schriebe. Lange Namen sind nicht automatisch lesbarer. Ein hilfreicher Doc-Kommentar ist oft wertvoller als ein extra langer Name.

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 o.ä.: Reader, Writer, Formatter, CloseNotifier 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 Semikolons 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 an."

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 Semikolons 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.

Eine Konsequenz der Semikolon-Einfüge-Regeln ist die, dass Sie die öffnende geschweiften Klammer einer Kontrollstruktur (if, for, switch oder select) nicht in die nächste Zeile setzen können. Dann würde ein Semikolon vor der geschweiften Klammer eingefügt mit 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, doch 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; break- und continue-Anweisungen akzeptieren eine optionale Sprungmarke, die anzeigt, was unterbrochen oder fortgeführt werden soll. 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 Erfolgsfall 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 und erneute Zuweisung

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.

*) Es sollte hier noch erwähnt werden, dass der Gültigkeitsbereich von Funktionsparametern und Rückgabewerten in Go der Funktionsrumpf ist, auch wenn sie außerhalb der den Rumpf einschließenden geschweiften Klammern erscheinen.

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 Semikolons 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 Kanal lesen, überall dort kann die range-Klausel die Schleife managen.

for key, value := range oldMap {
    newMap[key] = value
}

Benötigen Sie nur den ersten Rückgabewert (den Schlüssel bzw. Index), so verzichten Sie einfach auf den zweiten:

for key := range m {
    if key.ungueltig() {
        delete(m, key)
    }
}

Brauchen Sie dagegen nur den zweiten Rückgabewert (den Inhalt), so benutzen Sie den Leeren Bezeichner, einen Unterstrich, um den ersten zu verwerfen:

sum := 0
for _, value := range array {
    sum += value
}

Der Leere Bezeichner hat vielerlei Nutzen, der in einem späteren Abschnitt beschrieben wird.

Bei Strings nimmt Ihnen range noch mehr Arbeit ab: es bricht die einzelnen Unicode-Kodenummern der UTF-8-Kodierung auf. Dabei werden fehlerhafte Kodierungen byteweise betrachtet und jeweils durch die Rune U+FFFD ersetzt. (Der Name rune, gleichzeitig ein Standardtyp, ist Gos Bezeichnung für eine einzelne Unicode-Kodenummer. Einzelheiten dazu finden Sie in der Go Sprachbeschreibung (de).) Die Schleife:

for pos, char := range "日本\x80語" { // \x80 ist als UTF-8-Kodierung ungültig
    fmt.Printf("Zeichen %#U beginnt an Byteposition %d\n", char, pos)
}

druckt:

Zeichen U+65E5 '日' beginnt an Byteposition 0
Zeichen U+672C '本' beginnt an Byteposition 3
Zeichen U+FFFD '�' beginnt an Byteposition 6
Zeichen U+8A9E '語' beginnt an Byteposition 6

Und schließlich: Go kennt keinen Komma-Operator, und ++ und -- sind Anweisungen und keine Ausdrücke. Wenn also Ihre for-Schleife mehrere Laufvariablen hat, dann sollten Sie Mehrfachzuweisung nutzen, auch wenn das ++ und -- ausschließt.

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

Auch wenn sie hier längst nicht so verbreitet sind wie in anderen C-artigen Sprachen, können auch in Go break-Anweisungen benutzt werden, um ein switch vorzeitig zu beenden. Manchmal kann es sogar nötig sein, nicht nur aus dem Switch, sondern auch aus der sie umschließenden Schleife herauszuspringen. Das erreicht man in Go dadurch, dass die Schleife eine Sprungmarke bekommt und break dorthin springt. Das folgende Beispiel zeigt beides:

Loop:
	for n := 0; n < len(src); n += size {
		switch
		case src[n] < sizeOne:
			if validateOnly {
				break
			}
			size = 1
			update(src[n])

		case src[n] < sizeTwo:
			if n+1 >= len(src) {
				err = errShortInput
				break Loop
			}
			if validateOnly {
				break
			}
			size = 2
			update(src[n] + src[n+1]<<shift)
		}
	}

Natürlich akzeptiert auch die continue-Anweisung eine Sprungmarke; aber das betrifft nur Schleifen.

Zum Abschluss hier noch eine Vergleichsroutine für Byte-Slices mit zwei switch-Anweisungen:

// Compare vergleicht zwei Byte-Slices 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
}

Typ-Switch

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 den entsprechenden Typ haben. Go-typisch ist außerdem, dass der Name wiederverwertet wird, wodurch für jeden der Fälle eine neue Variablen desselben Namens aber von unterschiedlichem Typ deklariert wird.

var t interface{}
t = wertIrgenteinesTyps()
switch t := t.(type) {
default:
    fmt.Printf("unbekannter Typ %T\n", t)      // %T druckt jeden unbekannten Typ
case bool:
    fmt.Printf("bool %t\n", t)                 // t ist vom Typ bool
case int:
    fmt.Printf("int %d\n", t)                  // t ist vom Typ int
case *bool:
    fmt.Printf("Zeiger auf ein bool %t\n", *t) // t ist vom Typ *bool
case *int:
    fmt.Printf("Zeiger auf ein int %d\n", *t)  // t ist vom Typ *int
}

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 eines Arguments, von dem die Adresse übergeben wurden.

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 der Write-Methode 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 Funktion, die sich ab einer Position eine Zahl aus einem Byte-Slice 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 Eingabe-Slice b nach Zahlen abzusuchen:

    for i := 0; i < len(b); {
        x, i = nextInt(b, 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 (aufschieben, zurückstellen) 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 zur Ausführungszeit 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 (built-in) Funktionen 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 Kanäle, und gibt einen initialisierten (nicht nullisierten) Wert vom Typ T (nicht *T) zurück. Das ist deshalb so, weil diese drei Typen unter der Haube für Referenzen auf Datenstrukturen stehen, 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 Kanäle 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 Kanäle, und es gibt keinen Zeiger zurück! Um explizit einen Zeiger zu erhalten, reservieren Sie mit new oder nehmen die Adresse eine Variable explizit.

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. Benutzen Sie stattdessen Slices.

Slices

Slices (Abschnitte) 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 enthalten Referenzen auf ein darunterliegendes Array, und wenn Sie ein Slice einem anderen zuweisen, beziehen sich beide auf dasselbe 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 (f *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 buf zu lesen, schneiden (to slice) 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
        n += nbytes
        if nbytes == 0 || e != nil {
            err = e
            break
        }
    }

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)]
    copy(slice[l:], data)
    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.

2-dimensionale Slices

Arrays und Slices in Go sind 1-dimensional. Um so etwas wie ein 2D-Array oder 2D-Slice zu erzeugen, muss man ein Array von Arrays oder ein Slice von Slices definieren, nämlich so:

type Transform [3][3]float64  // Ein 3x3 Array; eigentlich ein Array von Arrays
type LinesOfText [][]byte     // Ein Slice von Byte-Slices

Da die Länge von Slices variabel ist, ist es möglich, dass die inneren Slices verschieden lang sind. Das kann ganz normal sein, wie in unserem LinesOfText-Beispiel, denn jede Zeile hat ihre eigene Länge:

text := LinesOfText{
	[]byte("Es ist an der Zeit,"),
	[]byte("dass die coolen Ziesel"),
	[]byte("Schwung in die Party bringen."),
}

Es kann nötig sein, 2D-Slices vorweg bereitzustellen, beispielsweise zum Verarbeiten von Pixelzeilen. Das kann auf zweierlei Weise erreicht werden. Eine davon ist, jedes Slice einzeln zu reservieren; die andere ist, ein einziges Array zu reservieren und die einzelnen Slices darauf zeigen zu lassen. Welche Methode zu wählen ist, hängt von Ihrer Anwendung ab. Wachsen oder schrumpfen Slices möglicherweise, so sollte man sie unabhängig voneinander reservieren, damit nicht eventuell eine Zeile in die nächste hineinwächst; wenn nicht, so kann es effizienter sein, das Objekt mit nur einer Reservierung zu erzeugen. Die beiden Methoden seien hier kurz skizziert. Zuerst die Einzelzeilen-Methode:

// Zuerst das übergeordnete Slice reservieren.
picture := make([][]uint8, YSize) // eine Zeile je y
// Dann für alle Zeilen: je ein Slice reservieren.
for i := range picture {
	picture[i] = make([]uint8, XSize)
}

Und hier die Methode mit nur einer Reservierung, die in Einzelzeilen aufgeschnitten wird:

// Zuerst das übergeordnete Slice reservieren, wie oben.
picture := make([][]uint8, YSize) // eine Zeile je y
// Dann ein großes Slice reservieren, das alle Pixel enthält.
pixels := make([]uint8, XSize*YSize) // vom Typ []uint8, auch wenn picture vom Typ [][]uint8 ist
// Dann für alle Zeilen: je ein Slice vom jeweiligen Rest des pixels-Slice abschneiden.
for i := range picture {
	picture[i], pixels = pixels[:XSize], pixels[XSize:]
}

Maps

Maps sind zweckmäße und leistungsfähige Standard-Datenstrukturen, die Werte eines Typs (den Schlüssel) mit Werten eines anderen Typs (dem Element oder Wert) assoziieren. Schlüssel kann jeder Typ sein, für den der Gleichheitsoperator definiert ist, wie Ganzzahlen, Gleitkommazahlen, komplexe Zahlen, Strings, Zeiger, Interfaces (solange deren dynamische Typen Gleichheit unterstützen), Strukturen und Arrays. Slices dagegen können nicht als Map-Schlüssel dienen, weil für sie Gleichheit nicht definiert ist. Wie Slices enthalten Maps Referenzen auf eine darunterliegende Datenstruktur. Ü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 und Slices, 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 den Nullwert zurück, weil es den Eintrag 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, können Sie den Leeren Bezeichner (_) anstelle der üblichen Variable benutzen.

_, 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

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

Die formatierende Druckfunktionen fmt.Fprint & Co. akzeptieren als erstes Argument jedes Objekt, welches das io.Writer-Interface implementiert; die Variablen os.Stdout und os.Stderr sollten Ihnen bekannt 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, Slices, Strukturen 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 EST:-18000 MST:-25200 PST:-28800 UTC:0]

Bei Maps sortieren Printf & Co die Schlüssel lexikografisch. Beim Drucken einer Struktur 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, "EST":-18000, "MST":-25200, "PST":-28800, "UTC":0}

(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. (Das Format %q funktioniert auch mit Ganzzahlen und Runen, wobei eine Runenkonstante in Hochkomma produziert wird.) Darüberhinaus funktioniert %x mit Strings, Byte-Arrays und Byte-Slices 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 contra Werte.)

Unsere String-Methode kann Sprintf rufen, weil die Druckfunktionen eintrittsinvariant (reentrant) sind, und so gekapselt werden können. Ein Detail jedoch muss verstanden werden: die String-Methode mit dem Aufruf von Sprintf darf nicht so konstruiert sein, dass sie sich endlos selbst wieder aufruft. Das kann passieren, wenn der Sprintf-Aufruf versucht, den Empfänger direkt als String zu drucken, wodurch die Methode nochmal aufgerufen wird. Das ist ein beliebter Fehler und ist leicht zu machen, wie folgendes Beispiel zeigt:

type MyString string

func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", m) // Fehler: wird rekursiv wiederholt
}

Aber genauso leicht ist er zu vermeiden: Konvertieren Sie das Argument in einen Standard-String, weil der diese Methode nicht kennt.

type MyString string
func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", string(m)) // OK: beachte die Konversion
}

Im Kapitel Initialisierung werden wir eine andere Technik kennenlernen, mit der die Rekursion vermieden wird.

Eine weitere Methode des Druckens gibt die Argumente einer Druckroutine direkt an eine andere Druckroutine weiter. 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.

Über das 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 selbstgestrickten 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 der Objekte, sogar der verschiedenen 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, Zeichen(Runen), Strings oder Bool'sche-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 beliebigen benutzerdefinierten Typ binden kann, ist es für einen beliebigen Wert möglich, sich selbst zum Drucken zu formatieren. Meist kommt diese Technik bei Strukturen zur Anwendung, sie ist aber auch nützlich für Skalartypen, wie zum Beispiel den Gleitkommatyp ByteSize:

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

Der Ausdruck YB wird als 1.00YB gedruckt, und ByteSize(1e13) ergibt 9.09TB.

Die Verwendung von Sprintf beim Implementieren der String-Methode von ByteSize ist hier sicher, d.h. rekursionsfrei, und zwar nicht wegen einer Konversion, sondern weil Sprintf mit %f aufgerufen wird, welches kein String-Format ist; Sprintf würde nur dann String rufen, wenn es einen String benötigt, %f aber benötigt einen Gleitkommawert.

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")
    gopath = os.Getenv("GOPATH")
)

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.) "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 = "/home/" + user
    }
    if gopath == "" {
        gopath = home + "/go"
    }
    // gopath kann durch den Parameter --gopath auf der Kommandozeile überschrieben werden.
    flag.StringVar(&gopath, "gopath", gopath, "GOPATH überschreiben")
}
	

Methoden

Zeiger contra Werte

Wie wir am Beispiel ByteSize gesehen haben, können Methoden für jeden namensbehafteten Typ definiert werden, nur nicht für Zeiger oder Interfaces; der Empfängertyp braucht keine Struktur 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 namensbehafteten 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 in der oben definierten Append-Funktion
}

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. Nimmt man nun Zeiger oder Wert als Empfängerobjekt? Die Regel dazu 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.

Das kommt daher, weil Zeigermethoden das Empfängerobjekt verändern können; würden sie an einen Wert gebunden, so würde der Methode eine Kopie des Werts übergeben und damit jede Änderung verloren gehen. Deshalb erlaubt Go diesen Fehler nicht. Aber es gibt eine nützliche Ausnahme. Wenn der Wert adressierbar (de) ist, so kümmert sich die Sprache um den häufig auftretenden Fall des Aufrufs einer Zeigermethode auf einen Wert, indem sie den Adressoperator automatisch einfügt. In unserem Beispiel ist die Variable b adressierbar, so dass wir ihre Write-Methode mit b.Write aufrufen können. Der Compiler macht daraus für uns ein (&b).Write.

Übrigens, Write auf ein Byte-Slice anzuwenden, ist die zentrale Idee hinter der Implementierung von bytes.Buffer.

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 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 + "]"
}

Konvertierungen

Die String-Methode von Sequence wiederholt, was Sprint bereits für Slices tut. (Außerdem hat es die armselige Komplexität O(N²).) Wir können die Arbeit verteilen (und gleichzeitig beschleunigen), wenn wir Sequence nach []int konvertieren, bevor wir Sprint rufen.

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

Dies ist ein weiteres Beispiel dafür, wie mithilfe der Konversionstechnik Sprintf von der String-Methode sicher gerufen werden kann. Da die beiden Typen Sequence und []int, abgesehen vom Namen, gleich sind, ist die Konversion von einem zum anderen erlaubt. Die Konversion erzeugt keinen neuen Wert, sie tut nur vorübergehend so, als ob der vorhandene Wert einen neuen Typ hätte. (Es gibt aber andere erlaubte Konvertierungen, von Ganzzahl zu Gleitkommazahl zu Beispiel, die tatsächlich einen neuen Wert erzeugen.)

Es ist typischer Go-Stil, den Typ eines Ausdrucks zu konvertieren, um so 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 {
    s = s.Copy()
    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 eher ungewohnt, kann aber sehr effektiv sein.

Interface-Konversionen und Typzusicherungen

Der Typ-Switch ist eine Form von Konversion: er nimmt ein Interface und konvertiert es gewissermaßen für jede Case-Klausel in den jeweiligen Typ. Hier eine vereinfachte Version des Kodes, mit dem fmt.Printf mithilfe eines Typ-Switch einen Wert in einen String verwandelt. Wenn dieser bereits ein String ist, wollen wir den tatsächlichen String-Wert des Interface, wenn er eine String-Methode besitzt, so wollen wir den Ergebniswert des Methodenaufrufs.

type Stringer interface {
    String() string
}

var value interface{} // value wird vom Aufrufer bereitgestellt
switch str := value.(type) {
case string:
    return str
case Stringer:
    return str.String()
}

Die erste Case-Klausel findet einen konkreten Wert; die zweite konvertiert das Interface in ein anderes Interface. Es ist absolut in Ordnung, Typen so zu mischen.

Was ist aber, wenn uns nur ein Typ interessiert? Wenn wir wissen, dass der Wert einen string enthält und wir diesen entnehmen wollen? Ein Switch mit nur einer Case-Klausel würde funktionieren, aber genauso gut tut es eine Typzusicherung. Eine Typzusicherung nimmt einen Interface-Wert und extrahiert daraus den Wert des explizit genannten Typs. Die Syntax ähnelt der der Eröffnungsklausel des Switch, nur mit einem explizit genannten Typ anstatt des Schlüsselworts type:

value.(Typname)

Das Ergebnis ist ein neuer Wert mit dem statischen Typ Typname. Dieser Typ muss entweder der konkrete Typ im Interface sein, oder ein zweiter Interface-Typ, in den der Wert konvertiert werden kann. Wenn wir wissen, dass der Wert einen String enthält, können wir schreiben:

str := value.(string)

Wenn sich nun herausstellt, dass der Wert doch keinen String enthält, dann wird das Programm mit einem Laufzeitfehler auf die Nase fallen. Damit dies nicht geschieht, benutzen wir die "Komma-Ok"-Schreibweise; die testet sicher, ob der Wert ein String ist oder nicht:

str, ok := value.(string)
if ok {
    fmt.Printf("Wert des String ist: %q\n", str)
} else {
    fmt.Printf("Wert ist kein String\n")
}

Scheitert die Typzusicherung, so wird str existieren, aber mit seinem Nullwert, dem leeren String.

Zur Verdeutlichung hier noch eine if-else-Anweisung, die dem Typ-Switch am Anfang des Abschnitts entspricht:

if str, ok := value.(string); ok {
    return str
} else if str, ok := value.(Stringer); ok {
    return str.String()
}

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 der Wert kein interessantes Verhalten über das im Interface beschriebene hinaus hat. 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(dst, src []byte)
    Decrypt(dst, src []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 bestimmten Verschlüsselungs-Algorithmus und eine bestimmte 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 Bearbeiter (handler) gebaut werden. Hier eine einfache Implementierung eines Bearbeiters 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!) In einem echten Server müsste man noch ctr.n vor konkurrierenden Zugriffen schützen; siehe dazu die Pakete sync und atomic.

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 eine Struktur? 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 Kanal, 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() {
    fmt.Println(os.Args)
}

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-Bearbeiter benutzt werden.  Wenn f eine Function
// mit der passenden Signatur ist, dann ist HandlerFunc(f) ein
// Handler-Object, das f ruft.
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP ruft f(w, 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 Kanal 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) {
    fmt.Fprintln(w, os.Args)
}

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 Bearbeiter für 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(w, req) innerhalb von HandlerFunc.ServeHTTP ruft. Dann werden die Argumente angezeigt.

In diesem Abschnitt haben wir aus einer Struktur, 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.

Der Leere Bezeichner

Den Leeren Bezeichner haben wir jetzt schon mehrmals im Zusammenhang mit for-range-Schleifen und mit Maps erwähnt. Ihm kann ein beliebiger Wert von beliebigem Typ zugewiesen oder er kann mit einem solchen deklariert werden, wobei der Wert rückstandsfrei entsorgt wird. Das ähnelt dem Schreiben nach /dev/null unter Unix: Er steht für einen Nurschreib-Wert, der eine Variable ersetzt, deren Wert irrelevant ist. Er ist nützlich über das bisher Bekannte hinaus.

Der Leere Bezeichner in einer Mehrfachzuweisung

Der Einsatz des Leeren Bezeichners in einer for-range-Schleife ist ein Spezialfall einer allgemeinen Situation: der Mehrfachzuweisung.

Erfordert eine Zuweisung mehrere Werte auf der linken Seite, wovon einer nicht weiter vom Programm benutzt wird, so vermeidet man es mit dem Einsatz des Leeren Bezeichner auf der linken Seite, eine unnütze Variable zu erzeugen, und man macht klar, dass der Wert verworfen werden soll. Ist beispielsweise nach dem Aufruf einer Funktion, die einen Wert und einen Fehler zurückliefert, nur der Fehler interessant, so benutzen Sie den Leeren Bezeichner, um den irrelevanten Wert zu verwerfen.

if _, err := os.Stat(path); os.IsNotExist(err) {
    fmt.Printf("%s existiert nicht\n", path)
}

Hin und wieder sieht man Kode, der einen Fehler-Wert verwirft, also den Fehler ignoriert; das ist von Übel. Prüfen Sie alle zurückgegeben Fehler; es gibt sie nicht ohne Grund.

// Übel! Dieses Programm stürzt ab, wenn der Pfad nicht existiert.
fi, _ := os.Stat(path)
if fi.IsDir() {
    fmt.Printf("%s ist ein Verzeichnis\n", path)
}

Nicht benutzte Imports und Variablen

Es ist ein Fehler, ein Paket zu importieren oder eine Variable zu deklarieren aber dann nicht zu benutzen. Unbenutzte Imports blähen ein Programm auf und verlangsamen das Kompilieren, während eine Variable, die initialisiert aber nicht benutzt wurde, eine Operation verschwendet und wahrscheinlich Anzeichen für einen gravierenderen Fehler ist. Während ein Programm aktiv entwickelt wird, treten unbenutzte Imports und Variable immer wieder auf, und es kann ziemlich lästig sein, sie nur für eine erfolgreiche Umwandlung löschen zu müssen, um sie später dann doch wieder zu brauchen. Mit dem Leeren Bezeichner umgeht man das Problem.

Das folgende unvollendete Programm hat zwei nicht benutzte Imports (fmt und io) und eine unbenutzte Variable (fd), weshalb die Umwandlung scheitert; es wäre aber schön zu wissen, ob der restliche Kode soweit korrekt ist:

package main

import (
    "fmt"
    "io"
    "log"
    "os"
)

func main() {
    fd, err := os.Open("test.go")
    if err != nil {
        log.Fatal(err)
    }
    // TODO: fd benutzen!
}

Damit der Compiler nicht weiter "unused imports" meckert, benutzen Sie den Leeren Bezeichner für je ein Symbol der importierten Pakete. Ganz ähnlich verhindert die Zuweisung von fd zum Leeren Bezeichner die "unused variable"-Fehlermeldung. Diese Programmversion wird erfolgreich kompiliert.

package main

import (
    "fmt"
    "io"
    "log"
    "os"
)

var _ = fmt.Printf // nur zur Fehlersuche; danach löschen!
var _ io.Reader    // nur zur Fehlersuche; danach löschen!

func main() {
    fd, err := os.Open("test.go")
    if err != nil {
        log.Fatal(err)
    }
    // TODO: fd benutzen!
    _ = fd
}

Gute Praxis ist, die globalen Deklarationen zum Verhindern von Import-Fehlermeldungen direkt hinter die Imports zu setzen und zu kommentieren; so findet man sie später leichter und kann dann aufräumen.

Importieren für Nebeneffekte

Unbenutzte Imports wie fmt oder io im letzten Beispiel sollen letztendlich benutzt oder wieder entfernt werden: Leere Zuweisungen kennzeichnen Kode, der noch in Arbeit ist. Es kann aber nützlich sein, ein Paket nur für den Nebeneffekt zu importieren, ganz ohne es zu benutzen. Beispielsweise registriert das Paket net/http/pprof in seiner init-Funktion HTTP-Handler mit nützlichen Diagnoseinformationen. Es hat zwar auch eine exportierte Programmierschnittstelle (API), doch genügt den meisten Klient-Programmen die Registrierung, während sie die Daten über eine Web-Seite handhaben. Um das Paket nur für den Nebeneffekt zu importieren, geben Sie dem Paket als Namen den Leeren Bezeichner:

import _ "net/http/pprof"

Diese Form des Imports macht klar, dass das Paket nur für seine Nebeneffekte importiert wird; das Paket kann gar nicht anders benutzt werden, weil es keinen Namen hat. (Hätte es einen und wir würden ihn nicht benutzen, so würde der Compiler das Programm zurückweisen.)

Prüfen von Interfaces

Wie wir bei der Erörterung der Interfaces weiter oben gesehen haben, braucht man in Go nicht explizit zu deklarieren, dass ein Typ ein Interface implementiert. Stattdessen implementiert der Typ das Interface, indem er einfach dessen Methoden implementiert. In der Praxis geschehen die meisten Interface-Konversionen statisch und werden so während der Umwandlung geprüft. Beispielsweise wird die Übergabe eines *os.File an eine Funktion, welche einen io.Reader erwartet, scheitern, solange *os.File nicht das Interface io.Reader implementiert.

Andere Interface-Prüfungen jedoch geschehen zur Laufzeit. Zum Beispiel definiert das Paket encoding/json ein Interface namens Marshaler. Trifft der JSON-Kodierer auf einen Typ, der dieses Interface implementiert, so benutzt er nicht mehr die Standardkonvertierung, sondern lässt den Typ sich selbst nach JSON konvertieren. Der JSON-Kodierer prüft diese Eigenschaft zur Laufzeit mit einer Typzusicherung:

m, ok := val.(json.Marshaler)

Wenn nur wichtig ist, zu wissen, ob ein Typ ein Interface implementiert, ohne es zu benutzen, zum Beispiel im Rahmen einer Fehlerprüfung, so benutzen Sie den Leeren Bezeichner, um den Wert zu ignorieren:

if _, ok := val.(json.Marshaler); ok {
    fmt.Printf("Wert %v vom Typ %T implementiert json.Marshaler\n", val, val)
}

Eine solche Situation tritt dann ein, wenn innerhalb des Pakets, welches den Typ implementiert, garantiert werden muss, dass es dem Interface genügt. Wird nun ein Typ — sagen wir json.RawMessage — dafür vorgesehen, seine JSON-Repräsentation individuell selbst einzurichten, so sollte er den json.Marshaler implementieren, nur, dass es hier keine statischen Konversionen gibt, die der Compiler automatisch prüfen könnte. Wenn nun der Typ versehentlich nicht dem Interface genügt, so wird der JSON-Kodierer nach wie vor funktionieren, jedoch die individuelle Implementierung nicht benutzen. Um zu garantieren, das die Implementierung korrekt ist, kann innerhalb des Pakets eine globale Deklaration mit dem Leeren Bezeichner benutzt werden:

var _ json.Marshaler = (*RawMessage)(nil)

In dieser Deklaration erfordert die Zuweisung mit Ihrer Konversion der *RawMessage zu einem Marshaler, dass *RawMessage den Marshaler implementiert, und diese Eigenschaft wird zum Umwandlungszeitpunkt geprüft. Sollte sich jemals das json.Marshaler-Interface ändern, dann wird sich das Paket nicht länger kompilieren lassen, und wir bemerken, dass eine Überarbeitung ansteht.

Der Leere Bezeichner in diesem Konstrukt zeigt an, dass die Deklaration nur der Typprüfung dient, und nicht dazu, eine Variable zu erzeugen. Tun Sie so etwas nicht für jeden Typ, der einem Interface genügen soll. Typischerweise werden solche Deklarationen nur dann gebraucht, wenn es nicht schon andere statische Konversionen im Kode gibt; das ist aber selten der Fall.

Einbetten

Go kennt nicht den üblichen, typgeleiteten Begriff der Unterklasse, man kann aber Teile einer Implementierung "ausleihen", 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. 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 Struktur-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 Struktur auf, gibt ihnen aber keine Namen:

// ReadWriter stores pointers to a Reader and a Writer.
// It implements 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 Strukturen 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 namensbehafteten Feld:

type Job struct {
    Command string
    *log.Logger
}

Der Typ Job besitzt jetzt auch Print, Printf, Println 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.Println("los geht's...")

Der Logger ist ein reguläres Feld der Struktur Job, das wir, wie üblich, im Konstruktor von Job 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, genauso wie weiter oben in der Read-Methode unserer ReadWriter-Struktur. Wenn wir nun hier Zugriff brauchen auf den *log.Logger der Variablen job vom Typ Job, dann schreiben wir job.Logger. Das ist hilfreich, um die Methoden von Logger zu verfeinern:

func (job *Job) Printf(format string, args ...interface{}) {
    job.Logger.Printf("%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 die Job-Struktur 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 übergeordneter Ansatz macht die Zugriffkontrolle über Kanäle 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 neben 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 nebenläufig aus; warte nicht aufs Ergebnis.

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

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

Funktionsliterale in Go sind Funktionsabschlüsse (closures): 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...

Kanäle

Wie Maps so werden auch Kanäle mit make erzeugt und der Ergebniswert davon funktioniert wie eine Referenz auf die darunterliegende Datenstruktur. 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 Kanal für Ganzzahlen
cj := make(chan int, 0)        // ungepufferter Kanal für Ganzzahlen
cs := make(chan *os.File, 100) // gepufferter Kanal für Dateizeiger

Ungepufferte Kanäle kombinieren Kommunikation, d.i. die Übermittlung eines Wertes, mit Synchronisation, d.i. die Garantie, dass sich zwei Berechnungen (Goroutinen) in einem definierten Zustand befinden.

Mit Kanälen 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, heißt das: warten 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, um die Ampel für den nächsten "Verbraucher" freizuschalten. 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 init() {
    for i := 0; i < MaxOutstanding; i++ {
        sem <- 1
    }
}

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

Wenn schließlich MaxOutstanding-mal via handle der process ausgeführt wird, wird jedes weitere handle blockieren beim Versuch, in den bereits vollen Kanalpuffer zu senden; zumindest so lange, bis ein anderes fertig wird und aus dem Kanal empfängt.

Dieses Konzept hat aber noch ein Problem: Serve erzeugt für jede ankommende Anfrage eine neue Goroutine, selbst wenn maximal MaxOutstanding gleichzeitig laufen können. Das kann dazu führen, dass das Programm unbegrenzt Ressourcen verbraucht, wenn die Anfragen zu schnell hintereinander hereinkommen. Diese Schwachstelle können wir beheben, indem wir Serve so abändern, dass die Erzeugung von Goroutinen begrenzt wird. Hier ist eine offensichtliche Lösung (doch Vorsicht, sie enthält einen Fehler, den wir gleich korrigieren werden):

func Serve(queue chan *Request) {
    for req := range queue {
        sem <- 1
        go func() {
            process(req) // Fehler; siehe die nachfolgende Erklärung
            <-sem
        }()
    }
}

Der Fehler ist der folgende: In einer for-Schleife wird die Schleifenvariable für jede Iteration wiederverwendet, so dass sich alle Goroutinen dieselbe Variable req teilen. Das ist nicht, was wir wollen. Wir müssen sicherstellen, dass jede Goroutine ihr eigenes req besitzt. Eine Möglichkeit ist, der Goroutine den Wert von req als Argument mitzugeben:

func Serve(queue chan *Request) {
    for req := range queue {
        sem <- 1
        go func(req *Request) {
            process(req)
            <-sem
        }(req)
    }
}

Vergleichen Sie diese Version mit der vorhergehenden: Sehen Sie die Unterschiede bei Deklaration und Aufruf des Funktionsabschlusses? Eine andere Lösung wäre, eine neue Variable mit dem gleichen Namen wie folgt zu erzeugen:

func Serve(queue chan *Request) {
    for req := range queue {
        req := req // Erzeuge eine neue Instanz von req für die Goroutine.
        sem <- 1
        go func() {
            process(req)
            <-sem
        }()
    }
}

Es mag seltsam aussehen:

req := req

zu schreiben, aber es ist legal und typisch für Go. Sie erhalten ein frisches Exemplar der Variablen mit demgleichen Namen, die mit Absicht die Schleifenvariable für jede Goroutine lokal aber eindeutig überdeckt.

Gehen wir nun zum ursprünglichen Problem, nämlich den Server zu programmieren, zurück. Eine weitere Lösung, welche Ressourcen gut handhabt, ist es, eine festgelegte Anzahl von handle-Goroutinen zu starten, 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, wenn 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 *Request, 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 Kanäle 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 (demultiplexing).

Im Beispiel aus dem vorigen Abschnitt war handle ein idealisierter Bearbeiter 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 Idee ist das Verteilen von Berechnungen auf mehrere CPU-Kerne. Wenn die Berechnung in Teile aufgespalten werden kann, die unabhängig laufen können, 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 numCPU = 4 // Anzahl der CPU-Kerne

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

Anstatt einen konstanten Wert füt numCPU zu definieren, können wir auch die Laufzeitumgebung nach einem geeigneten Wert fragen. Die Funktion runtime.NumCPU gibt die Anzahl der CPU-Kerne der Hardware zurück; also können wir schreiben:

var numCPU = runtime.NumCPU()

Außerdem gibt es eine Funktion runtime.GOMAXPROCS, die die benutzerdefinierte Anzahl der Kerne zurückgibt (oder festlegt), die in einem Go-Programm gleichzeitig arbeiten dürfen. Vorgabewert ist runtime.NumCPU, doch der kann überschrieben werden durch die ähnlich benannte Umgebungsvariable oder, indem man die Funktion mit einem positiven Ganzzahlwert aufruft. Aufruf mit Null gibt nur den aktuellen Wert zurück. Wenn wir also die Benutzervorgabe respektieren wollen, so schreiben wir:

var numCPU = runtime.GOMAXPROCS(0)

Achtung! Verwechseln Sie nicht die Idee, ein Programm nebenläufig zu strukturieren, so dass Komponenten unabhängig ausgeführt werden können, mit parallel ausgeführten Rechnungen, um Mehrfach-CPUs effizient auszunutzen. Wenn auch mit den Nebenläufigkeits-Eigenschaften von Go leicht einige Probleme in parallele Berechnungen aufgeteilt werden können, so ist Go doch eine nebenläufige und keine parallele Sprache; nicht alle Parallelisierungsprobleme passen zu Go. Der Unterschied wird erörtert in diesem Blog- Artikel.

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 (garbage collector) 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.

Fehler

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 ein Fehlertext, der 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 der Operation oder des Pakets voranstellen, in welchem der Fehler aufgetreten ist. Im Paket image zum Beispiel lautet der Fehlertext, falls 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.Create(filename)
    if err == nil {
        return
    }
    if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC {
        deleteTempFiles() // Platz schaffen.
        continue
    }
    return
}
Die zweite if-Anweisung hier ist eine weitere Typzusicherung. Wenn sie fehlschlägt, wird ok = false und e = nil sein. 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.

Panik

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 bekommt 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.

// 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 Slice-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 eine vereinfachte Version des regexp-Pakets; 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 namensbehaftete 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 — weil die Methode an einen Typ gebunden ist, ist es auch in Ordnung, ja sogar natürlich, dafür denselben Namen zu wählen, wie den des Standardtyps error — ganz einfach Parse-Fehler melden, ohne dass man den Parse-Stack per Hand aufdröseln müsste:

if pos == 0 {
    re.error("'*' illegal at start of expression")
}

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 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"
    "html/template"
    "log"
    "net/http"
)

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="https://chart.apis.google.com/chart?chs=300x300&cht=qr&choe=UTF-8&chl={{.}}" />
<br>
{{.}}
<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 html/template ist sehr leistungsfähig; unser Programm kratzt gerade mal an seinen Möglichkeiten. Im Kern überschreibt es ein Stück HTML-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.

Die zwei Schnipsel {{"{{.}}"}} besagen, dass die Daten, die der Schablone als Suchsring präsentiert werden, auf der Web-Seite angezeigt werden sollen. Das HTML-Schablonenpaket erledigt auch automatisch eine angemessene Ersatzkodierung (escaping), so dass die Textanzeige sicher ist.

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 leistungsfähig genug, um eine Menge in wenigen Zeilen passieren zu lassen.