Go Home Page
Die Programmiersprache Go

A Tutorial for the Go Programming Language — Deutsche Übersetzung

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

Eine Anleitung zum Programmieren in Go

Einleitung

Dies ist eine Einführung in die Grundlagen der Programmiersprache Go. Sie wendet sich an Programmierer, die vertraut sind mit C oder C++. Diese Einführung ist keine erschöpfende — was dem zur Zeit am nächsten kommt, ist die "Language Specification". Nach dieser Einführung möchten Sie sich vielleicht noch "Effective Go" anschauen, welches tiefer eintaucht in den Gebrauch der Sprache Go. Außerdem gibt es Folien zu einem 3-Tage-Kurs: "Tag 1", "Tag 2", "Tag 3".

Um das Charakteristische der Sprache zu zeigen, durchschreiten wir der Reihe nach mehrere kleine Programme. Allesamt sind sie lauffähig (jedenfalls waren sie es, als dies hier geschrieben wurde) und im Verzeichnis /doc/progs/ zu finden.

Kode-Ausschnitte sind mit den Zeilenummern der Originaldateien versehen, für mehr Klarheit bleiben die Leerzeilen leer.

Hallo, Welt

Fangen wir an, wie üblich:

 
05    package main

07    import fmt "fmt"  // Paket für formatierte Ein- und Ausgabe

09    func main() {
10        fmt.Printf("Hallo, Welt; oder Καλημέρα κόσμε; oder こんにちは 世界\n")
11    }
		

Jede Go-Quelldatei deklariert mit einer package-Anweisung, zu welchem Paket sie gehört. Sie darf andere Pakete importieren, um deren Funktionen zu nutzen. Dieses Programm hier importiert das Paket fmt, um Zuggriff auf unser altbekanntes, nun großgeschriebenes und mit Paketnamen qualifiziertes fmt.Printf zu bekommen.

Funktionen werden mit dem Schlüsselwort func eingeleitet. Das Programm startet (nach jedweder Initialisierung) mit der Funktion main im Paket main.

String-Konstanten können mit UTF-8 kodierte Unicode-Zeichen enthalten. (Es ist sogar so, dass Go-Quelldateien als UTF-8-kodiert definiert sind.)

Die Konvention für Kommentare ist diegleiche wie in C++:

    /* ... */
    // ...
		

Zum Thema "Drucken" wird später noch viel mehr zu sagen sein.

Semikolon

Sie haben vielleicht bemerkt, dass in unserem Programm kein Semikolon vorkommt. In Go entdeckt man es typischerweise nur als Trenner für die Klauseln von for-Schleifen oder ähnlichen Konstrukten; man braucht sie nicht nach jeder Anweisung.

Tatsächlich ist es so, dass die formale Sprache Semikola benutzt, ähnlich wie C oder Java, und dass automatisch eins eingefügt wird, wenn ein Zeilenende wie das Ende einer Anweisung aussieht. Man muss es nicht selbst eintippen.

Wie das im Detail gemacht wird, können Sie in der Sprachbeschreibung nachlesen. Für die Praxis genügt es zu wissen, dass man am Zeilenende kein Semikolon braucht. (Es kann aber sein, dass Sie welche einfügen müssen, wenn Sie mehrere Anweisungen pro Zeile schreiben.) Außerdem hilfreich: auch vor einer schließenden geschweiften Klammer ist das Semikolon überflüssig.

Diese Herangehensweise sorgt für sauber aussehenden, semikolonfreien Kode. Etwas anderes überrascht vielleicht: dass nämlich die öffnende geschweifte Klammer nach einem Konstrukt wie der if-Anweisung auf derselben Zeile wie das if stehen muss; ist das nicht der Fall, kann die Übersetzung scheitern oder es kann zu einem falschen Ergebnis führen. Die Sprache erzwingt diesen Klammerstil zu einem gewissen Grad.

Kompilieren

Go-Programme werden kompiliert. Zur Zeit gibt es zwei Compiler. Gccgo ist ein Go-Compiler, der das "back-end" von GCC benutzt. Weiterhin gibt es eine Garnitur von Compilern mit jeweils verschiedenen (und seltsamen) Namen für verschiedene Architekturen: 6g für den 64-Bit x86, 8g für den 32-Bit x86, und andere. Diese Compiler arbeiten deutlich schneller, generieren aber weniger effizienten Kode als gccgo. Zur Zeit (Ende 2009) besitzen sie außerdem ein robusteres Laufzeitsystem, aber gccgo holt auf.

Und so kompilieren und starten wir unser Programm. Mit 6g:

    $ 6g helloworld.go  # kompilieren; Ausgabe ist helloworld.6
    $ 6l helloworld.6   # binden; Ausgabe ist 6.out
    $ 6.out
    Hallo, Welt; oder Καλημέρα κόσμε; oder こんにちは 世界
    $
		

Mit gccgo sieht es traditioneller aus:

    $ gccgo helloworld.go
    $ a.out
    Hallo, Welt; oder Καλημέρα κόσμε; oder こんにちは 世界
    $
		

Echo

Als nächstes haben wir hier eine Version des Unix-Dienstprogramms echo(1):

 
05    package main

07    import (
08        "os"
09        "flag"  // Parser für Kommandozeilenparameter
10    )

12    var omitNewline = flag.Bool("n", false, "ohne Zeilenvorschub am Ende")

14    const (
15        Space = " "
16        Newline = "\n"
17    )

19    func main() {
20        flag.Parse()   // Absuchen der Parameterliste und setzen der Schalter
21        var s string = ""
22        for i := 0; i < flag.NArg(); i++ {
23            if i > 0 {
24                s += Space
25            }
26            s += flag.Arg(i)
27        }
28        if !*omitNewline {
29            s += Newline
30        }
31        os.Stdout.WriteString(s)
32    }
		

Dieses Programm ist zwar kurz aber zeigt einiges Neues. Im vorigen Beispiel haben wir gesehen, wie func eine Funktion einleitete. Die Schlüsselworte var, const und type (hier nicht benutzt) leiten ebenfalls Deklarationen ein. Merke, dass Deklarationen derselben Art innerhalb von Klammern gruppiert werden können, jeweils eine Position pro Zeile wie in den Zeilen 7—10 und 14—17. Aber das ist nicht zwingend — wir hätten auch sagen können:

    const Space = " "
    const Newline = "\n"
		

Das Programm importiert das "os"-Paket, um auf dessen Variable Stdout zuzugreifen, die vom Typ *os.File ist. Die import-Anweisung ist eigentlich eine Deklaration: In ihrer allgemeinen Form, wie wir sie im Hallo-Welt-Programm benutzt haben, benennt sie die Kennung (fmt), die man dazu benutzt, um auf Elemente des Pakets, das aus der Datei ("fmt") importiert wird, zuzugreifen, die wiederum im aktuellen oder einem Standard-Verzeichnis gefunden wird. Hier hingegen haben wir den expliziten Namen beim Importieren weggelassen; als Voreinstellung wird der Name benutzt der durch das importierte Paket definiert ist, was per Konvention natürlich der Dateiname selbst ist. Unser Hallo-Welt-Programm hätte also nur sagen brauchen: import "fmt".

Natürlich können Sie Ihre eigenen Import-Namen angeben, aber nötig ist das nur, wenn Namenskonflikte zu lösen sind.

Mit gegebenem os.Stdout können wir seine WriteString-Methode benutzen um die Zeichenkette zu drucken.

Nachdem das flag-Paket importiert ist, erzeugt die Zeile 12 eine globale Variable zur Aufnahme des Schalters -n. Die Variable omitNewline ist vom Typ *bool, also ein Zeiger auf ein bool.

In main.main durchsuchen wir zunächst die Parameter (Zeile 20), und erzeugen dann eine lokale String-Variable für die Ausgabe.

Die Deklaration hat die Form

    var s string = ""
		

und besteht aus den Schlüsselwort var gefolgt vom Variablennamen gefolgt vom Datentyp gefolgt von einem Gleichheitszeichen und dem Anfangswert der Variablen.

Go versucht, prägnant zu sein, und diese Deklaration kann man kürzen. Weil ein String-Konstante vom Typ string ist, müssten wir das dem Compiler nicht extra sagen. Wir könnten schreiben:

    var s = ""
		

oder sogar noch kürzer:

    s := ""
		

Der :=-Operator wird in Go häufig genutzt, um eine Deklaration plus Anfangswert auszudrücken. Einer findet sich in der for-Klausel auf der nächsten Zeile:

 
22        for i := 0; i < flag.NArg(); i++ {
		

Das flag-Paket hat die Parameter durchsucht und hält die, die keine Schalter sind, in einer Liste bereit, über die in naheliegender Weise iteriert werden kann.

Die for-Anweisung in Go unterscheidet sich gleich mehrfach von der in C. Erstens ist sie das einzige Schleifenkonstrukt — es gibt kein while oder do. Zweitens hat die Klausel keine runden Klammern aber die geschweiften Klammern um den Rumpf sind obligatorisch. Das gilt auch für die if- und die switch-Anweisung. Spätere Beispiele werden zeigen, wie die for-Schreibweise variiert werden kann.

Der Schleifenrumpf erzeugt die Zeichenkette s, indem er mit += die Parameter und Zwischenräume angehängt. Wenn -n nicht gesetzt ist, kommt ans Ende noch ein Zeilenvorschub. Schließlich wird das Ergebnis ausgegeben.

Beachten Sie: main.main ist eine Funktion ohne Parameter und ohne Rückgabewert. Sie ist so definiert. Wird das Ende von main.main erreicht, heißt das: Erfolg! Wollen Sie einen Fehler melden, rufen Sie:

    os.Exit(1)
		

Im os-Paket gibt's weitere wichtige Sachen für den Programmanfang. Zum Beispiel ist os.Args ein Slice (Abschnitt), das vom flag-Paket benutzt wird, um an die Kommandozeilen-Parameter zu kommen.

Zwischenspiel: Datentypen

Go hat vertraute Datentypen wie int und float, die ihre Werte in "maschinen-angemessener" Größe darstellen. Go definiert aber auch Typen mit expliziter Größenangabe wie int8, float64, und so weiter, außerdem Ganzzahlen ohne Vorzeichen wie uint, uint32, etc. Dies sind alles eigene Datentypen — selbst wenn int und int32 beide 32 Bit lang sind, sind sie nicht vom selben Typ. Es gibt auch den Typ byte, synonym zu int8, welches der Elementtyp für Strings ist.

Apropos, string ist ebenfalls ein integrierter Datentyp. Strings-Werte sind unveränderlich — sie sind nicht einfach nur byte-Arrays. Ein String-Wert, einmal erzeugt, kann nicht mehr verändert werden. Aber natürlich können Sie String-Variablen ändern, einfach durch eine neue Zuweisung. Dieser Schnipsel aus strings.go ist gültiger Kode:

 
11        s := "hello"
12        if s[1] != 'e' { os.Exit(1) }
13        s = "good bye"
14        var p *string = &s
15        *p = "ciao"
		

Dagegen sind folgende Anweisungen ungültig, weil sie String-Werte verändern würden:

    s[0] = 'x'
    (*p)[1] = 'y'
		

Go-Strings ähneln den const strings in der C++-Welt, während der Zeiger auf einen String das Analogon zu einer const string-Referenz ist.

Ja, es gibt Zeiger. Indes, Go vereinfacht ihren Gebrauch — lesen Sie weiter.

Arrays werden so definiert:

    var arrayOfInt [10]int
		

Arrays sind Werte, wie Strings auch, aber sie sind änderbar. Das ist ein Unterschied zu C, wo arrayOfInt als Zeiger zu einem int-Wert benutzt werden kann. Weil Go-Arrays Werte sind, ist es sinnvoll (und nützlich) über Zeiger zu Arrays zu sprechen.

Die Größe eines Arrays ist Bestandteil seines Typs — aber man kann eine slice-Variable deklarieren und ihr einen Zeiger zu einem Array mit demselben Elementtyp zugeweisen, oder, weit häufiger, einen slice-Ausdruck der Form a[low : high], der das Unter-Array vom Index low bis zum Index high -1 repräsentiert. Slices sind Arrays sehr ähnlich, haben aber keine explizite Größe ([] gegenüber [10]) und sind Referenz auf einen Ausschnitt eines darunterliegenden, oft anonymen, echten Arrays. Mehrere Slices können sich Daten teilen, wenn sie Teile desselben Arrays repräsentieren, mehrere Arrays können das nie!

Slices sind in Go-Programmen weit üblicher als echte Arrays; sie sind flexibler, sie sind semantisch gesehen Referenzen und sie sind effizient. Was ihnen fehlt, ist die Kontrolle über das Speicherlayout, wie das echte Array sie hat; wenn Sie genau hundert Elemente eines Arrays in Ihrer Struktur haben wollen, nehmen Sie ein Array!

Um ein Array an eine Funktion zu übergeben, werden sie den formalen Parameter fast immer als Slice deklarieren wollen. Beim Funktionsaufruf nehmen Sie die Adresse des Arrays, und Go erzeugt daraus (effizient) eine Slice-Referenz und übergibt diese.

Mit Slices kann man eine Funktion so schreiben (aus sum.go):

 
09    func sum(a []int) int {   // gibt ein int zurück
10        s := 0
11        for i := 0; i < len(a); i++ {
12            s += a[i]
13        }
14        return s
15    }
		

und sie so aufrufen:

 
19        s := sum(&[3]int{1,2,3})  // ein Abschnitt eines Arrays wird an sum übergeben
		

Schauen Sie, wie der Rückgabetyp (int) für sum() definiert wird: hinter der Parameterliste. Der Ausdruck [3]int{1,2,3} — ein Datentyp gefolgt von einem Ausdruck in geschweiften Klammern — ist ein Konstruktor für einen Wert, in diesem Fall für ein Array aus 3 ints. Das & davor gibt uns die Adresse einer Instanz dieses Wertes. Wir übergeben den Zeiger an sum() und befördern ihn damit (implizit) zu einem Slice.

Wenn Sie ein echtes Array erzeugen, und der Compiler soll für Sie die Elemente zählen, dann benutzen Sie ... als Array-Größe:

    s := sum(&[...]int{1,2,3})
		

In der Praxis aber, außer Sie nehmen es sehr genau mit dem Layout Ihrer Datenstruktur, in der Praxis ist ein Slice — leere eckige Klammern ohne & — alles, was Sie brauchen:

    s := sum([]int{1,2,3})
		

Des weiteren gibt es Maps, die so initialisiert werden können:

    m := map[string]int{"eins":1 , "zwei":2}
		

Die integrierte Funktion len(), die die Anzahl der Elemente liefert, erschien uns zuerst in sum. Sie funktioniert mit Strings, Arrays, Slices, Maps und Channels.

Was ebenfalls mit Strings, Arrays, Slices, Maps und Channels funktioniert, ist die range-Klausel in for-Schleifen. Statt mit:

    for i := 0; i < len(a); i++ { ... }
		

über die Elemente eines Slices (oder einer Map oder ...) zu iterieren, können wir schreiben:

    for i, v := range a { ... }
		

Das weist i den Index und v den Wert des jeweiligen Elements des Zielbereichs zu. Mehr Anwendungsbeispiele gibt's in Effective Go.

Zwischenspiel: Speicherzuteilung

Die meisten Datentypen in Go sind Werte. Ob int oder struct oder Array, jede Zuweisung kopiert den Inhalt des Objekts. Um Speicher für eine neue Variable zu reservieren, benutzen Sie new(), welches einen Zeiger auf den zugeteilten Speicher zurückgibt:

    type T struct { a, b int }
    var t *T = new(T)
		

oder Go-typischer:

    t := new(T)
		

Einige Datentypen — Maps, Slices und Channels (siehe unten) — sind, semantisch gesehen, Referenzen. Wenn Sie den Inhalt eines Slice oder einer Map ändern, sind auch die anderen Variablen, die dieselben Daten referenzieren, von der Änderung betroffen. Für diese drei Datentypen benutzen Sie die integrierte Funktion make():

    m := make(map[string]int)
		

Diese Anweisung initialisiert eine neue Map, bereit mit Inhalt gefüllt zu werden, wohingegen die Deklaration:

    var m map[string]int
		

nur eine nil-Referenz erzeugt, die nichts enthält. Um die Map nutzen zu können, müssen Sie die Referenz mit make() oder durch Zuweisung aus einer bestehenden Map erst initialisieren.

Beachten Sie, dass new(T) den Typ *T, aber make(T) den Typ T zurückgibt. Wenn Sie (versehentlich) ein Referenz-Objekt mit new() reservieren, bekommen Sie einen Zeiger auf eine nil-Referenz zurück, was dasgleiche wäre wie das Deklarieren einer nicht initialisierten Variablen, von der Sie dann die Adresse benutzen wollten.

Zwischenspiel: Konstanten

Wenn auch Ganzzahlen in Go in vielen Größen daherkommen, so gilt das nicht für Integer-Konstanten. Es gibt keine Konstanten wie 0LL oder 0x0UL. Satt dessen werden Integer-Konstanten als Werte hoher Genauigkeit behandelt; ein Wert kann nur überlaufen, wenn er einer Integer-Variablen zugewiesen werden, die zu klein ist, um ihn aufnehmen zu können.

    const hardEight = (1 << 100) >> 97  // erlaubt
		

Manche Feinheiten würden den Verweis auf das Juristen-Englisch der "Sprachbeschreibung" verdienen; hier sollen ein paar kommentierte Beispiele genügen:

    var a uint64 = 0  // a ist vom Typ uint64, Wert 0
    a := uint64(0)    // äquivalent; benutzt eine "Konvertierung"
    i := 0x1234       // i kriegt den Default-Typ: int
    var j int = 1e6   // erlaubt - 1000000 kann durch int dargestellt werden
    x := 1.5          // Float
    i3div2 := 3/2     // Ganzzahldivision - Ergebnis ist 1
    f3div2 := 3./2.   // Fließkommadivision - Ergebnis ist 1.5
		

Konvertieren funktioniert nur in einfachen Fällen, wie das Konvertieren von ints verschiedener Größen mit oder ohne Vorzeichen, und zwischen ints und floats, und in wenigen einfachen anderen Fällen. Es gibt es keine automatische Zahlenkonvertierung in Go außer, wenn Konstanten während der Zuweisung zu einer Variablen konkrete Größe und konkreten Typ bekommen.

Ein I/O-Paket

Als nächstes schauen wir uns ein einfaches Paket für Datei-Ein/Ausgaben an, welches mit den üblichen Operationen open/close/read/write arbeitet. So fängt file.go an:

 
05    package file

07    import (
08        "os"
09        "syscall"
10    )

12    type File struct {
13        fd      int    // Dateinummer (Deskriptor), intern
14        name    string // Dateiname fürs Open
15    }
		

Die ersten Zeilen vereinbaren den Paketnamen file, und importieren dann zwei weitere Pakete. Das os-Paket versteckt die Unterschiede verschiedener Betriebssysteme und erlaubt eine konsistente Sicht auf Dateien und anderes; hier werden wir seine Werkzeuge zur Fehlerbehandlung nutzen sowie seinen Kern der Dateiein-/ausgabe.

Ein weiteres Thema wird das niedrig angesiedelte syscall-Paket sein, das eine einfache Schnittstelle zu den darunterliegenden Systemaufrufen anbietet.

Das nächste ist eine Typdefinition: das Schlüsselwort type leitet eine Typdeklaration ein, in diesem Fall für eine Datenstruktur mit Namen File. Und, um's ein bischen interessanter zu machen, enthält unser File den Dateinamen auf den die Dateinummer deutet.

Weil File mit einem Großbuchstaben beginnt, steht dieser Typ auch außerhalb des Pakets zur Verfügung, d.h. für die Nutzer des Pakets. Die Regel für Sichtbarkeit von Informationen in Go ist simpel: Wenn der Name (eines "top-level"-Typs, einer Funktion oder Methode, einer Konstanten oder Variablen, eines Strukturfelds oder einer Strukturmethode) mit einem Großbuchstaben beginnt, können Paketnutzer ihn sehen. Wenn nicht sind Name und das damit bezeichnete Ding nur innerhalb des Paket sichtbar, in dem sie deklariert sind. Das ist mehr als eine Konvention: die Regel wird durch den Compiler erzwungen. In Go benutzen wir für öffentlich sichtbare Namen den Begriff "exportiert".

In File beginnen alle Feldnamen mit Kleinbuchstaben, sind also unsichtbar für die Benutzer, aber wir werden gleich ein paar exportierte, großgeschriebene Methoden hinzufügen.

Zunächst aber noch eine Fabrikmethode, um ein File zu erzeugen:

 
17    func newFile(fd int, name string) *File {
18        if fd < 0 {
19            return nil
20        }
21        return &File{fd, name}
22    }
		

Diese gibt einen Zeiger auf die neue File-Struktur zurück, Dateinummer und -name gefüllt. Der Kode nutzt die Idee eines "zusammengesetzten Literals", wie sie in Go auch für Maps und Arrays gebraucht werden, zur Konstruktion eines neuen Objekts auf dem Heap. Wir hätten auch schreiben können:

    n := new(File)
    n.fd = fd
    n.name = name
    return n
		

aber für eine so simple Struktur wie File ist es einfacher, die Adresse eines Ad-hoc-zusammengesetzten Literals zu retournieren, wie das auf Zeile 21 geschieht.

Die Fabrikmethode benutzen wir, um ein paar altbekannte "exportierte" Variablen vom Typ *File zu konstruieren:

 
24    var (
25        Stdin  = newFile(0, "/dev/stdin")
26        Stdout = newFile(1, "/dev/stdout")
27        Stderr = newFile(2, "/dev/stderr")
28    )
		

Die Funktion newFile wurde nicht exportiert, weil sie eine interne ist. Die geeignetere Methode zum Exportieren ist Open:

 
30    func Open(name string, mode int, perm int) (file *File, err os.Error) {
31        r, e := syscall.Open(name, mode, perm)
32        if e != 0 {
33            err = os.Errno(e)
34        }
35        return newFile(r, name), err
36    }
		

Auf diesen wenigen Zeilen gibt es wieder einiges Neue zu entdecken. Einmal gibt Open mehrere Werte zurück, eine Datei und einen Fehler (davon gleich mehr). Wir deklarieren die mehrfachen Rückgabewerte in einer geklammerten Deklarationsliste; der Syntax nach schauen sie aus wie eine zweite Parameterliste. Die Funktion syscall.Open gibt ebenfalls mehrere Werte zurück, die wir mit der Mehrfach-Variablen-Deklaration von Zeile 31 abgreifen; für die zwei Werte werden r und e deklariert, beide vom Typ int (auch wenn man erst im syscall-Paket nachschauen müsste, um das sehen zu können). Schließlich gibt die Zeile 35 zwei Werte zurück: einen Zeiger auf das neue File und einen Fehler. Wenn syscall.Open scheitert, ist die Dateinummer negativ und newFile gibt nil zurück.

Nun zu Fehlern: Die os-Bibliothek umfasst auch ein allgemeines Fehlermodell. Es ist ratsam, dessen Möglichkeiten in der eigenen Schnittstelle zu nutzen, damit die Fehlerbehandlung im gesamten Go-Kode konsistent bleibt — wir tun es hier auch. In Open benutzen wir eine Konversion, um den Unix-Integerwert errno in einen Integertyp os.Errno zu stecken, der os.Error implementiert.

Jetzt, wo wir Files erzeugen können, können wir auch Methoden für sie schreiben. Um eine Typmethode zu deklarieren, definieren wir eine Funktion mit einem Empfänger des gewünschten Typs in Klammern vor dem Funktionsnamen. Hier ein paar Methoden für *File, für die jeweils eine Empfängervariable file deklariert wird.

 
38    func (file *File) Close() os.Error {
39        if file == nil {
40            return os.EINVAL
41        }
42        e := syscall.Close(file.fd)
43        file.fd = -1  // damit nicht nochmal geschlossen werden kann
44        if e != 0 {
45            return os.Errno(e)
46        }
47        return nil
48    }

50    func (file *File) Read(b []byte) (ret int, err os.Error) {
51        if file == nil {
52            return -1, os.EINVAL
53        }
54        r, e := syscall.Read(file.fd, b)
55        if e != 0 {
56            err = os.Errno(e)
57        }
58        return int(r), err
59    }

61    func (file *File) Write(b []byte) (ret int, err os.Error) {
62        if file == nil {
63            return -1, os.EINVAL
64        }
65        r, e := syscall.Write(file.fd, b)
66        if e != 0 {
67            err = os.Errno(e)
68        }
69        return int(r), err
70    }

72    func (file *File) String() string {
73        return file.name
74    }
		

Es gibt kein implizites this. Um die Elemente der Struktur anzusprechen, muss die Empfängervariable benutzt werden. Methoden werden nicht innerhalb der struct-Deklaration deklariert. Die struct-Deklaration enthält nur Datenelemente. Methoden können für fast alle Typen definiert, auch für Integer, auch für Arrays und nicht nur für structs. Ein Array-Beispiel folgt später.

Die String-Methode nennen wir so wegen einer Druckkonvention, die wir später beschreiben wollen.

Die Methoden benutzen die öffentliche Variable os.EINVAL, um den Unix-Fehlerkode EINVAL (in der os.Error-Version) zurückzugeben. Die os-Bibliothek definiert einen Standard solcher Fehlerwerte.

Jetzt können wir unser neues Paket benutzen:

 
05    package main

07    import (
08        "./file"
09        "fmt"
10        "os"
11    )

13    func main() {
14        hello := []byte("Hallo, Welt\n")
15        file.Stdout.Write(hello)
16        file, err := file.Open("/gibts/nicht",  0,  0)
17        if file == nil {
18            fmt.Printf("Kann Datei nicht öffnen; Error=%s\n", err.String())
19            os.Exit(1)
20        }
21    }
		

Mit dem Kode "./" beim Import von "./file" nimmt der Compiler unser eigenes Paket anstatt eines aus dem Verzeichnis der installierten Pakete.

Endlich darf unser Programm laufen:

    % helloworld3
    Hallo, Welt
    Kann Datei nicht öffnen; Error=No such file or directory
    %
		

Ein verrückendes "cat"

Hier ist, aufbauend auf dem file-Paket, eine vereinfachte Version des Unix-Dienstes cat(1), progs/cat.go:

 
05    package main

07    import (
08        "./file"
09        "flag"
10        "fmt"
11        "os"
12    )

14    func cat(f *file.File) {
15        const NBUF = 512
16        var buf [NBUF]byte
17        for {
18            switch nr, er := f.Read(&buf); true {
19            case nr < 0:
20                fmt.Fprintf(os.Stderr, "cat: Fehler beim Lesen von %s: %s\n", f.String(), er.String())
21                os.Exit(1)
22            case nr == 0:  // EOF
23                return
24            case nr > 0:
25                if nw, ew := file.Stdout.Write(buf[0:nr]); nw != nr {
26                    fmt.Fprintf(os.Stderr, "cat: Fehler beim Schreiben von %s: %s\n", f.String(), ew.String())
27                }
28            }
29        }
30    }

32    func main() {
33        flag.Parse()   // Absuchen der Parameterliste und setzen der Schalter
34        if flag.NArg() == 0 {
35            cat(file.Stdin)
36        }
37        for i := 0; i < flag.NArg(); i++ {
38            f, err := file.Open(flag.Arg(i), 0, 0)
39            if f == nil {
40                fmt.Fprintf(os.Stderr, "cat: Kann %s nicht öffnen: Fehler=%s\n", flag.Arg(i), err)
41                os.Exit(1)
42            }
43            cat(f)
44            f.Close()
45        }
46    }
		

Dem solten Sie inzwischen leicht folgen können, nur die switch-Anweisung bringt nochmal etwas Neues. Genauso wie eine for-Schleife können if und switch eine Initialisierungs-Anweisung enthalten. Das switch auf Zeile 18 benutzt eine solche, um die Variablen nr und er zu erzeugen, die die Rückgabewerte von f.Read() aufnehmen. (Das if auf Zeile 25 tut ähnliches.) Die switch-Anweisung ist eine allgemeine, d.h. sie wertet die Cases aus, von oben nach unten bis zum zum ersten zutreffenden, und die Case-Ausdrücke brauchen weder Konstanten noch etwa Integer zu sein, wenn sie nur alle vom selben Typ sind.

Da der switch-Wert hier nur true ist, könnten wir ihn auch weglassen — dasgleiche gilt für die for-Anweisung: ein fehlender Wert bedeutet true. Solch ein switch ist eigentlich eine if-else-Kette. Und, wo wir gerade dabei sind, soll auch erwähnt werden, dass jedes case implizit mit break endet.

Zeile 25 ruft Write() und schneidet dabei den Eingabepuffer auf, der selbst wieder ein Slice ist. Slices sind die Go-typische Art, mit Ein-/Ausgabepuffern umzugehen.

Bauen wir nun eine Variante von cat, die optional den rot13-Algorithmus auf die Eingabe anwendet. Das ginge auch ganz einfach byteweise, aber stattdessen wollen wir die Go-Variante des Interface benutzen.

Die Routine cat() benutzt nur zwei Methoden von f, nämlich Read() und String(). Definieren wir also zunächst ein Interface mit genau diesen beiden Methoden. Hier der Kode aus progs/cat_rot13.go:

 
26    type reader interface {
27        Read(b []byte) (ret int, err os.Error)
28        String() string
29    }
		

Man sagt, dass jeder Typ mit den beiden Methoden von reader — egal, welche Methoden es außerdem noch gibt — dieses Interface implementiert. Weil file.File diese Methoden implementiert, implementiert es auch das reader-Interface. Wir könnten jetzt an cat herumfummeln, so dass reader anstatt *file.File akzeptiert würde, aber machen wir's doch hübscher: Wir kreieren einen neuen Typ, der reader implementiert, der einen existierenden reader kapselt und außerdem rot13 auf die Daten anwendet. Dafür definieren wir den Typ und implementieren die Methoden, und ohne weitere Kodepflege erhalten wir eine zweite Implementierung von reader.

 
31    type rotate13 struct {
32        source    reader
33    }

35    func newRotate13(source reader) *rotate13 {
36        return &rotate13{source}
37    }

39    func (r13 *rotate13) Read(b []byte) (ret int, err os.Error) {
40        r, e := r13.source.Read(b)
41        for i := 0; i < r; i++ {
42            b[i] = rot13(b[i])
43        }
44        return r, e
45    }

47    func (r13 *rotate13) String() string {
48        return r13.source.String()
49    }
50    // Ende der rotate13-Implementierung
	

(Die rot13-Funktion von Zeile 42 ist trivial und nicht wert, hier wiedergegeben zu werden.)

Um die neue Eigenschaft auch nutzen zu können, definieren wir einen Schalter:

 
14    var rot13Flag = flag.Bool("rot13", false, "rot13(Eingabe)")
		

und nutzen ihn in der kaum geänderten cat()-Funktion:

 
52    func cat(r reader) {
53        const NBUF = 512
54        var buf [NBUF]byte

56        if *rot13Flag {
57            r = newRotate13(r)
58        }
59        for {
60            switch nr, er := r.Read(&buf); {
61            case nr < 0:
62                fmt.Fprintf(os.Stderr, "cat: Fehler beim Lesen von %s: %s\n", r.String(), er.String())
63                os.Exit(1)
64            case nr == 0:  // EOF
65                return
66            case nr > 0:
67                nw, ew := file.Stdout.Write(buf[0:nr])
68                if nw != nr {
69                    fmt.Fprintf(os.Stderr, "cat: Fehler beim Schreiben von %s: %s\n", r.String(), ew.String())
70                }
71            }
72        }
73    }
		

(Wir hätten das Kapseln auch in main erledigen und damit cat() fast unberührt lassen können, mit Ausnahme des Parametertyps. Betrachten Sie das als Übungsaufgabe.) Die Zeilen 56—58 tun, was zu tun ist: Wenn der rot13-Schalter gesetzt ist, pack den mitgegebenen reader in ein rotate13 und mach weiter wie gewohnt. Beachten Sie, dass die Variablen des Interface Werte sind und keine Zeiger: der Parameter ist vom Typ reader und nicht *reader, auch wenn es unter der Haube einen Zeiger auf ein struct beherbergt.

Hier ist cat in Aktion:

    % echo abcdefghijklmnopqrstuvwxyz | ./cat
    abcdefghijklmnopqrstuvwxyz
    % echo abcdefghijklmnopqrstuvwxyz | ./cat --rot13
    nopqrstuvwxyzabcdefghijklm
    %
		

Anhänger der Dependency-Injection dürfen jubeln: so einfach ist es, die Implementierung einer Dateinummer zu ersetzen!

Interfaces sind ein charakteristisches Merkmal von Go. Ein Interface wird dann von einem Typ implementiert, wenn der Typ alle im Interface deklarierten Methoden implementiert. Das heißt auch, dass ein Typ beliebig viele verschiedene Intefaces implementieren kann. Es gibt keine Typ-Hierarchie, alles kann mehr ad hoc geschehen — wie wir bei rot13 sehen konnten. Der Typ file.File implementiert reader; er könnte genausogut writer implementieren, oder irgendein anderes Interface, das Methoden des Typs zusammenfasst — wie es die Situation gerade erfordert. Oder denken Sie nur an das leere Interface:

    type Empty interface {}
		

Jeder Typ implementiert das leere Interface, und das macht es nützlich für Container und ähnliches.

Sortieren

Interfaces bieten eine einfache Form des Polymorphismus. Sie trennen sauber die Definition dessen, was ein Objekt tut, davon, wie es das tut, und erlauben, dass unterschiedliche Implementierungen zu verschiedenen Zeiten dasselbe Interface vertreten.

Betrachten Sie zum Beispiel diesen einfachen Sortieralgorithmus aus progs/sort.go:

 
13    func Sort(data Interface) {
14        for i := 1; i < data.Len(); i++ {
15            for j := i; j > 0 && data.Less(j, j-1); j-- {
16                data.Swap(j, j-1)
17            }
18        }
19    }
		

Der Kode braucht nur drei Methoden, die wir im Interface von Sort kapseln:

 
07    type Interface interface {
08        Len() int
09        Less(i, j int) bool
10        Swap(i, j int)
11    }
		

Wir dürfen Sort auf jeden Typ anwenden, der Len, Less, und Swap implementiert. Das sort-Paket enthält die nötigen Methoden, für Arrays von Integern, Strings usw. Hier ist der Kode für int-Arrays:

 
33    type IntArray []int

35    func (p IntArray) Len() int            { return len(p) }
36    func (p IntArray) Less(i, j int) bool  { return p[i] < p[j] }
37    func (p IntArray) Swap(i, j int)       { p[i], p[j] = p[j], p[i] }
		

Das sind Methoden für Typen, die kein struct sind. Sie könnten aber Methoden für jeden beliebigen Typ definieren, der in Ihrem Paket definiert und benannt ist.

Und hier eine Testroutine aus progs/sortmain.go. Sie benutzt eine weitere Funktion aus dem sort-Paket, die hier der Kürze wegen weggelassen wurde; eine Funktion um zu testen, ob das Ergebnis sortiert vorliegt.

 
12    func ints() {
13        data := []int{74, 59, 238, -784, 9845, 959, 905, 0, 0, 42, 7586, -5467984, 7586}
14        a := sort.IntArray(data)
15        sort.Sort(a)
16        if !sort.IsSorted(a) {
17            panic("fail")
18        }
19    }
		

Wenn wir einen neuen Typ sortieren können wollen, müssen wir nur die drei Methoden für diesen Typ implementieren. Etwa so:

 
30    type day struct {
31        num        int
32        shortName  string
33        longName   string
34    }

36    type dayArray struct {
37        data []*day
38    }

40    func (p *dayArray) Len() int            { return len(p.data) }
41    func (p *dayArray) Less(i, j int) bool  { return p.data[i].num < p.data[j].num }
42    func (p *dayArray) Swap(i, j int)       { p.data[i], p.data[j] = p.data[j], p.data[i] }

Drucken

Die Beispiele für formatiertes Drucken bisher waren eher anspruchslos. In diesem Abschnitt zeigen wir, wie formatiertes Ein-/Ausgeben in Go gut gemacht wird.

Einfache Anwendungen des fmt-Pakets, welches Printf, Fprintf usw. implementiert, haben wir schon gesehen. Im fmt-Paket wird Printf mit dieser Signatur deklariert:

    Printf(format string, v ...) (n int, errno os.Error)
		

Das ... steht für die variabel lange Parameterliste, die in C mit dem stdarg.h-Makro gehandhabt würde, die aber in Go mittels eines leeren Interfaces (interface {}) weitergereicht wird, um dann mit der Reflektions-Bibliothek ausgepackt zu werden. Es gehört hier nicht zum Thema, aber der Gebrauch von Reflektion hilft, einige nette Eigenschaften des Go-Printf zu erklären, was damit zusammenhängt, dass Printf den Typ seiner Parameter dynamisch bestimmen kann.

Zum Beispiel muss in C jedes Format zum Typ des Parameters passen. Das ist in Go einfacher. Statt %llud sagen Sie einfach %d. Das Printf kennt die Größe und Vorzeicheneigenschaft des Integers und kann das Richtige für Sie tun. Das Kodestück:

 
10        var u64 uint64 = 1<<64-1
11        fmt.Printf("%d %d\n", u64, int64(u64))
		

druckt:

    18446744073709551615 -1
		

Und mehr: Wenn Sie faul sein möchten, druckt das Format %v jeden Wert, sogar ein Array oder eine Struktur, in einem einfachem, angemessenem Stil. Die Ausgabe von:

 
14        type T struct {
15            a int
16            b string
17        }
18        t := T{77, "Sunset Strip"}
19        a := []int{1, 2, 3, 4}
20        fmt.Printf("%v %v %v\n", u64, t, a)
		

ist:

    18446744073709551615 {77 Sunset Strip} [1 2 3 4]
		

Das Formatieren können Sie sich komplett sparen, wenn Sie Print oder Println anstatt Printf benutzen. Diese Routinen formatieren vollautomatisch. Print druckt einfach die Elemente mit einer Entsprechung von %v, während Println noch Leerzeichen zwischen den Parametern und einen Zeilenvorschub am Ende einfügt. Die Ausgabe beider folgenden Zeilen ist identisch mit der obigen vom Printf:

 
21        fmt.Print(u64, " ", t, " ", a, "\n")
22        fmt.Println(u64, t, a)
		

Wenn Printf oder Print einen Ihrer eigenen Typen formatieren soll, spendieren Sie diesem Typ die Methode String(), die einen String zurückgibt. Die Print-Routinen werden den Wert daraufhin untersuchen, ob er die Methode implementiert, und wenn ja, sie statt anderer Formatierungen auch benutzen. Hier ist ein einfaches Beispiel:

 
09    type testType struct {
10        a int
11        b string
12    }

14    func (t *testType) String() string {
15        return fmt.Sprint(t.a) + " " + t.b
16    }

18    func main() {
19        t := &testType{77, "Sunset Strip"}
20        fmt.Println(t)
21    }
		

Weil *testType die String()-Methode besitzt, wird die Standard-Formatierung sie benutzen und folgendes ausgeben:

    77 Sunset Strip
		

Beachten Sie, dass die String()-Methode fürs Formatieren Sprint (die naheliegend Go-Variante; sie gibt einen String zurück) ruft; spezielle Formatierer können die fmt-Bibliothek rekursiv benutzen.

Eine weitere Fähigkeit von Printf ist, dass es mit %T eine String-Darstellung des Typs zum Wert druckt; das kann nützlich sein bei der Fehlersuche in polymorphem Kode.

Komplett individualisierte Druckformate zu schreiben, ist möglich, mit Schaltern und Nachkommastellen und dem ganzen Drumherum, aber das führt uns zu weit vom Thema weg und soll Ihnen als Erkundungsübung bleiben.

Sie dürfen natürlich trotzdem fragen, wie Printf herausfindet, ob ein Typ die String()-Methode implementiert. Nun, eigentlich fragt es, ob der Wert in eine Interface-Variable passt, die diese Methode implementiert. Kurz skizziert tut es mit einer Variablen v folgendes:

    type Stringer interface {
        String() string
    }
		
    s, ok := v.(Stringer)  // Testen, ob v implementiert "String()"
    if ok {
        result = s.String()
    } else {
        result = defaultOutput(v)
    }
		

Der Kode benutzt eine Typprüfung (v.(Stringer)), um zu prüfen, ob der Wert in v dem Stringer-Interface genügt; wenn ja, wird s zur Interface-Variablen mit der implementierten Methode und ok wird auf true gesetzt. Dann benutzen wir die Interface-Variable zum Rufen der Methode. (Auch wenn wir es nur an dieser Stelle erwähnen, ist das "Komma, ok"-Muster der Go-typische Ausdruck, um Erfolg von Operationen zu testen, Operationen wie Typ-Konversionen, Map-Aktualisierung, Kommunikation, ...) Wenn der Wert dem Interface nicht genügt, wird ok auf false gesetzt.

In obigem Kodestück folgt der Name Stringer einer Konvention, die besagt, dass wir für Ein-Methoden-Interfaces wie dieses ein "[e]r" an den Methodenamen anhängen.

Eine letzte Besonderheit: Um die Sammlung komplett zu machen, gibt es neben Printf usw. und Sprintf usw. auch Fprintf usw. Anders als in C ist der erste Parameter von Fprintf keine Datei sondern eine Variable vom Typ io.Writer, einem Interface-Typ, definiert in der io-Bibliothek:

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

(Das ist ein weiteres Interface nach der genannten Konvention, dieses Mal für Write; es gibt auch io.Reader, io.ReadWriter und so fort.) Dadurch können Sie Fprintf auf jeden Typ anwenden, der eine Write()-Methode implementiert, also nicht nur auf Dateien sondern auch auf Netzwerk-Kanäle, Puffer, auf was immer sie wollen.

Primzahlen

Wir kommen jetzt zu Prozessen und ihrer Kommunikation — kurz: zu Nebenläufigkeit. Das ist ein weites Feld und weil wir uns kurzfassen möchten, setzen wir das Thema als bekannt voraus.

Ein Klassiker in diesem Zusammenhang ist das Primzahlensieb. (Das Sieb des Eratosthenes ist ein effizienterer Algorithmus als der hier präsentierte, wir sind aber im Moment mehr an Nebenläufigkeit als an Algorithmen interessiert.) Es bekommt einen Eingabestrom, bestehend aus allen natürlichen Zahlen, und schickt sie durch eine Reihe von Filtern, für jede Primzahl einen, um alle Vielfachen der jeweiligen Primzahl auszusortieren. In jedem Bearbeitungsschritt existieren die Filter aus den bis dahin ermittelten Primzahlen. Die nächste Zahl, die durchfällt, ist die nächste Primzahl und löst den Bau des nächsten Filters aus.

Hier ein Flussdiagramm: jedes Rechteck steht für ein Filterelement, dessen Bau durch die Zahl ausgelöst wurde, die als erste durch alle Elemente davor geschlüpft war:


     


Den Integerstrom erzeugen wir mit einem Go-Channel, einem Kommunikationskanal, der zwei nebenläufige Berechnungen verbindet (die Idee ist geborgt von den CSP-Abkömmlingen). Channel-Variablen in Go sind Referenzen auf Laufzeitobjekte, die die Kommunikation koordinieren; wie bei Maps oder Slices benutzt man make zum Erzeugen neuer Channels.

Die erste Funktion in progs/sieve.go ist:

 
09    // Schick die Folge 2, 3, 4, ... in den Kanal 'ch'.
10    func generate(ch chan int) {
11        for i := 2; ; i++ {
12            ch <- i  // Schick 'i' in den Kanal 'ch'.
13        }
14    }
		

Die generate-Funktion schickt 2, 3, 4, 5 usw. an seinen Parameter, den Kanal ch, und zwar mit dem binären Kommunikationsoperator <-. Channel-Operationen blockieren: wenn also kein Empfänger vorhanden ist, für den Wert in ch, dann wartet die Operation, bis einer verfügbar wird.

Die filter-Funktion hat drei Parameter: einen Eingabekanal, einen Ausgabekanal und eine Primzahl. Sie kopiert die Werte von der Eingabe zur Ausgabe, verwirft aber alles, was durch die Primzahl teilbar ist. Der unäre Kommunikationsoperator <- empfängt (ruft ab) den nächsten Wert aus dem Kanal.

 
16    // Kopiere die Werte vom Kanal 'in' zum Kanal 'out',
17    // lösch aber alle die, die durch 'prime' teilbar sind.
18    func filter(in, out chan int, prime int) {
19        for {
20            i := <-in  // Empfange einen neuen Wert für die Variable 'i' aus dem Kanal 'in'.
21            if i % prime != 0 {
22                out <- i  // Schick 'i' in den Kanal 'out'.
23            }
24        }
25    }
		

Generator und Filter arbeiten simultan. Go hat ein eigenes Modell für Prozess/Thread/leichtgewichtiger Prozess/Koroutine; um also Verwechslung zu vermeiden, nennen wir die nebenläufigen Berechnungen in Go Goroutinen. Um eine Goroutine zu starten, ruft man die Funktion auf und setzt davor das Schlüsselwort go; das startet die Funktion parallel zur laufenden Berechnung, aber im selben Adressraum:

    go sum(hugeArray) // Berechne die Summe im Hintergrund
		

Wenn Sie wissen wollen, wann die Berechnung beendet ist, schicken Sie einen Kanal mit, auf dem zurückgemeldet werden kann:

    ch := make(chan int)
    go sum(hugeArray, ch)
    // ... Tu eine Weile etwas anderes
    result := <-ch  // Warte aufs Ergebnis und ruf es ab
		

Zurück zu unserem Primzahlensieb. So wird die Pipeline zusammengebaut:

 
28    func main() {
29        ch := make(chan int)  // Erzeuge einen ersten Kanal.
30        go generate(ch)  // Starte generate() als Goroutine.
31        for {
32            prime := <-ch
33            fmt.Println(prime)
34            ch1 := make(chan int)
35            go filter(ch, ch1, prime)
36            ch = ch1
37        }
38    }
		

Zeile 29 erzeugt einen ersten Kanal für generate, welches dann gestartet wird. Sobald eine neue Primzahl aus dem Kanal heraushüpft, wird ein neuer filter an die Pipeline angehängt und dessen Ausgabe wird der neue Wert von ch.

Unser Siebprogramm kann man so zurechtbiegen, dass es ein für diese Art von Programmierung typisches Muster benutzt. Hier die variierte Form von generate, zu finden in progs/sieve1.go:

 
10    func generate() chan int {
11        ch := make(chan int)
12        go func(){
13            for i := 2; ; i++ {
14                ch <- i
15            }
16        }()
17        return ch
18    }
		

Diese Version macht alles intern. Es erzeugt den Ausgabekanal, setzt eine Goroutine in Gang, die ein Funktionsliteral laufen lässt und gibt den Kanal an den Rufenden zurück. Sie ist eine Fabrik für nebenläufiges Abarbeiten: Starten einer Goroutine und Rückgabe einer Verknüpfung zu ihr.

Die Schreibweise als Funktionsliteral (Zeilen 12—16) erlaubt uns, eine anonyme Funktion zu konstruieren und auf der Stelle aufzurufen. Beachten Sie, dass die lokale Variable ch dem Funktionsliteral zur Verfügung steht und auch gültig bleibt, nachdem generate geendet hat.

Diegleiche Änderung kann man an filter vornehmen:

 
21    func filter(in chan int, prime int) chan int {
22        out := make(chan int)
23        go func() {
24            for {
25                if i := <-in; i % prime != 0 {
26                    out <- i
27                }
28            }
29        }()
30        return out
31    }
		

Die Hauptschleife der sieve-Funktion wird dadurch einfacher und klarer, und, wo wir gerade dabei sind, machen wir doch daraus auch eine Fabrik:

 
33    func sieve() chan int {
34        out := make(chan int)
35        go func() {
36            ch := generate()
37            for {
38                prime := <-ch
39                out <- prime
40                ch = filter(ch, prime)
41            }
42        }()
43        return out
44    }
		

Die Schnittstelle zwischen main und Primzahlensieb ist nun ein Kanal für Primzahlen:

 
46    func main() {
47        primes := sieve()
48        for {
49            fmt.Println(<-primes)
50        }
51    }

Bündeln und Verteilen

Mithilfe von Kanälen ist es möglich, mehrere unabhängige Klient-Goroutinen zu bedienen ohne explizit einen Multiplexer zu schreiben. Der Trick ist, dem Server einen Kanal als Nachricht zu schicken, über den er dem Sender antworten kann. Ein echtes Klient-Server-Programm würde eine Menge Kode bedeuten; deshalb hier nur ein simpler Ersatz, der das Konzept illustriert. Wir beginnen mit der Definition eines request-Typs, mit eingebettetem Kanal, der beim Antworten benutzt wird.

 
09    type request struct {
10        a, b    int
11        replyc  chan int
12    }
		

Der Server wird einfach gestrickt: er wird einfache binäre Operationen auf Integer ausführen. Hier der Kode, der die Operation aufruft und die Anfrage beantwortet:

 
14    type binOp func(a, b int) int

16    func run(op binOp, req *request) {
17        reply := op(req.a, req.b)
18        req.replyc <- reply
19    }
		

Zeile 14 definiert den Namen binOp als Funktion mit zwei Ganzzahlen als Parameter und einer dritten als Rückgabewert.

Die server-Routine kreiselt ohne Ende, wobei sie Anfragen empfängt und, um durch langlaufende Operationen nicht blockiert zu werden, für die eigentliche Arbeit Go-Routinen startet.

 
21    func server(op binOp, service chan *request) {
22        for {
23            req := <-service
24            go run(op, req)  // nicht warten
25        }
26    }
		

Wir fabrizieren der Server auf gewohnte Weise, indem wir ihn starten und den angeschlossenen Kanal zurückliefern:

 
28    func startServer(op binOp) chan *request {
29        req := make(chan *request)
30        go server(op, req)
31        return req
32    }
		

Hier ein einfacher Test. Er startet den Server mit einer Additions-Operation und schickt N Anfragen los ohne auf die Antworten zu warten. Erst danach werden die Ergebnisse abgefragt und geprüft.

 
34    func main() {
35        adder := startServer(func(a, b int) int { return a + b })
36        const N = 100
37        var reqs [N]request
38        for i := 0; i < N; i++ {
39            req := &reqs[i]
40            req.a = i
41            req.b = i + N
42            req.replyc = make(chan int)
43            adder <- req
44        }
45        for i := N-1; i >= 0; i-- {   // egal in welcher Reihenfolge
46            if <-reqs[i].replyc != N + 2*i {
47                fmt.Println("Fehler bei", i)
48            }
49        }
50        fmt.Println("Fertig!")
51    }
		

Eine Schwachstelle dieses Programms ist, dass der Server nicht sauber beendet wird. Wenn main endet, wartet noch eine ganze Reihe von Go-Routinen auf Nachrichten. Um das abzustellen, bauen wir den Server mit einem zweiten, dem quit-Kanal:

 
32    func startServer(op binOp) (service chan *request, quit chan bool) {
33        service = make(chan *request)
34        quit = make(chan bool)
35        go server(op, service, quit)
36        return service, quit
37    }
		

Der quit-Kanal wird der server-Funktion übergeben, die ihn folgendermaßen benutzt:

 
21    func server(op binOp, service chan *request, quit chan bool) {
22        for {
23            select {
24            case req := <-service:
25                go run(op, req)  // nicht warten
26            case <-quit:
27                return
28            }
29        }
30    }
		

Im Kode von server entscheidet die select-Anweisung, welches case weitermachen darf, welcher Kanal bearbeitet werden kann. Wenn alle blockieren, wartet sie, bis eines davon weitermachen kann; wenn mehrere können, wählt es eines aus. In unserem Fall erlaubt das select dem Server, alle Anfragen zu akzeptieren, bis er eine quit-Nachricht erhält. Daraufhin springt er an sein Ende und damit heraus aus der Verarbeitungsschleife.

Alles, was noch zu tun bleibt, ist, am Ende von main den quit-Kanal "anzublinken":

 
40        adder, quit := startServer(func(a, b int) int { return a + b })

...

55        quit <- true
		

Es gäbe noch so viel mehr zu erzählen über Nebenläufigkeit und Go. Hier wollten wir Ihnen nur ein paar Grundlagen nahebringen.