Effective Go — Deutsche Übersetzung
- Das Original:
-
https://golang.org/doc/effective_go.html
Version of June 25, 2020 (go1.15) - Diese Übersetzung:
-
https://bitloeffel.de/DOC/golang/effective_go_20200814_de.html
Stand: 27.07.2020
© 2010-20 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 eigenes
Gestottere 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:
-
Die neue Deklaration befindet sich im selben Gültigkeitsbereich [englisch: scope, A.d.Ü]
wie die existierende Deklaration von
v
. (Wurdev
in einem Gültigkeitbereich höherer Stufe deklariert, so legt die neue Deklaration eine neue Variable an.*) -
Der zugewiesene Wert ist
v
zuweisbar. - Die Deklaration enthält mindestens eine Variable, die durch diese Deklaration neu deklariert wird.
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
- sind Arrays Werte. Zuweisung eines Arrays zu einem anderen kopiert alle Elemente.
- Insbesondere, wenn man ein Array an eine Funktion übergibt, erhält die Funktion eine Kopie des Array, und nicht einen Zeiger darauf.
-
Die Größe eines Array ist Bestandteil seines Typs;
[10]int
und[20]int
sind verschieden.
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.
"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 {{html "{{.}}"}}
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.