Syntax
In diesem Artikel wird die Syntax von Go anhand von Beispielen erläutert.
Das beliebte "Hello World" Beispiel sieht in Go so aus:
// hello.go
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
Das Programm kann direkt ausgeführt werden:
$ go run hello.go
Hello, World!
Dabei wird das Programm compiliert, in einem temporären Verzeichnis abgelegt und dort ausgeführt. Man kann es auch explizit bauen:
$ go build hello.go
$ ./hello
Hello, World!
Anhand dieses Programms sieht man einige Eigenschaften von Go:
- Kommentare werden, wie in C++, entweder zeilenweise mit // eingeleitet oder zeilenübergreifend mit /* begonnen und */ beendet.
- Jede Quelltextdatei gehört zu einem Package. Ein Package
kann aus einer oder mehrerer Dateien bestehen. Die erste Zeile einer
Quelltextdatei gibt an, zu welchem Package die Datei gehört. Ein
Programm (command) muss mindestens das Package
main
definieren, und darin muss es eine Funktionmain
geben. Eine Bibliothek (library) hat kein Packagemain
. - In einer Quelltextdatei können andere Packages importiert werden,
um auf deren Funktionen zuzugreifen. Hier wird das Package
fmt
(formatted I/O) importiert; es beinhaltet Funktion für die formatierte Ein- und Ausgabe. - Eine Funktion wird mit dem Keywort
func
eingeleitet. Die Hauptfunktionmain
wird zu Beginn eines Programmes aufgerufen. Der Inhalt der Funktion wird, wie in C/C++ oder Java, in geschweifte Klammern { } eingeschlossen. - Beim Zugriff auf Funktionen eines Package wird der Package-Name
(hier:
fmt
) dem Funktionsnamen (hier:Println
) vorangestellt. Die Funktion wird für die Ausgabe beliebiger Daten auf der Standardausgabe verwendet; sie setzt automatisch einen Zeilenumbruch hinter die Ausgabe. - Es wird kein Semikolon am Ende einer Anweisung benötigt. Mehrere Anweisungen können aber in einer Zeile geschrieben werden; sie werden dann durch das Semikolon getrennt.
Ein Go-Programm kann in mehrere Packages unterteilt werden.
Bei einem ausführbaren Programm (command) muss mindestens das Package
main
vorhanden sein. Eine Bibliothek besteht i.d.R. aus einem
package, kann aber auch mehrere (interne) Packages beinhalten.
Innerhalb eines Package werden Variablen, Konstanten, Funktionen, Typen, Methoden und Interfaces definiert. Nur wenn deren Name mit einem Großbuchstaben beginnt, sind sie "exportiert", d.h. von außen (in anderen Packages, die dieses Package importieren) sichtbar. Ansonsten können die Bezeichner nur innerhalb des Package verwendet werden.
Andere Package werden mit import "Package"
eingebunden. Es dürfen nur
Packages eingebunden werden, die auch tatsächlich benutzt werden. Ein
zyklisches Einbinden ist nicht erlaubt; wenn also Package A ein Package
B importiert, darf B weder direkt noch indirekt A importieren.
Ein Package kann aus einer oder mehrerer Dateien bestehen. Üblicherweise befinden sich diese im selben Verzeichnis, und der Name des Verzeichnisses entspricht dem Package-Namen.
Packages werden mit vollem Pfadnamen importiert, z.B. import "net/http"
.
Bei der Benutzung der Funktion wird nur der Package-Name angegeben, z.B.
http.ListenAndServe()
. Bei Mehrdeutigkeiten ist es möglich, den
Namespace lokal umzuschreiben, z.B. durch import rnd crypto/rand
. Die
Funktionen der Bibliothek sind nun mit rnd.Name()
erreichbar.
Seit Go 1.11 gibt es das Konzept der Module. Ein Modul besteht aus einem oder mehreren Packages. Zudem enthält die Modulspezifikation eine Angabe der vorausgesetzten Go-Version sowie Versionsbezeichnungen der abhängigen Module. Zu Modulen wird es hier demnächst einen eigenen Artikel geben.
// Beispiele für Deklarationen
var a int
var b int8 = 1
var c = int64(2)
Der Deklaration einer Variablen wird das Schlüsselwort var
vorangestellt. Der Typ steht hinter der Variablen. Optional kann eine
Initialisierung (Zuweisung eines Wertes) erfolgen. Der Typ kann
weggelassen werden, wenn er aus der Initialisierung bereits hervor geht.
Bei einer Deklaration ohne Initialisierung wird die Variable automatisch
mit einem Defaultwert initialisiert. Dies ist, je nach Typ, die Null,
der leere String, der logische Wert false
oder der Nullpointer nil
.
Es ist erlaubt, eine Variable zu nutzen, ohne ihr vorher (explizit)
einen Wert zugewiesen zu haben.
// nur in Funktionen erlaubt:
d := 3 // entspricht: var d int = 3
Innerhalb von Funktionen gibt es eine weitere Variante der
Variablendeklaration, bei der der Typ und das Schlüsselwort var
weggelassen werden können. Statt des Zeichens = wird := benutzt. Der Typ
der Variable ergibt sich aus dem Typ des Ausdrucks rechts des :=
Operators.
Eingebaute (skalare) Datentypen in Go sind:
-
bool
für Wahrheitswerte (false
odertrue
) -
int
ist ein maschinenabhängiger, vorzeichenbehafteter Integer-Typ. Auf 32-Bit Systemen ist er 32, auf 64-Bit Systemen 64 Bit breit. -
int8 int16 int32 int64
sind maschinenunabhängige, vorzeichenbehaftete Integer-Typen der angegebenen Breite in Bit. -
uint uint8 uint16 uint32 uint64
sind analog dazu vorzeichenlose Integer-Typen. -
float32 float64
sind 32 bzw. 64 Bit breite Gleitkomma-Typen. -
complex64 complex128
sind Typen für komplexe Zahlen. Eincomplex64
besteht aus zweifloat32
Komponenten, eincomplex128
aus zweifloat64
. -
byte
ist ein Alias füruint8
und wird für Zeichen in US-ASCII benutzt. -
rune
ist ein Alias fürint32
und wird für Unicode Codepoints in UTF-8 Kodierung genutzt. -
string
ist ein Datentyp für eine (konstanten) UTF-8 Zeichenkette. -
uintptr
ist ein Typ für Zeiger. Die Breite ist plattformabhängig, es gilt nur die allgemeine Aussage, dass alle Zeiger darin speicherbar sein müssen. -
error
ist ein eingebauter Typ für die Fehlerbehandlung.
// Konstanten
const Pi float64 = 3.14159265358979323846
const zero = 0.0
const x, y, z = 5, 17.3, "foo"
const ti int = 42 // typisierte Konstante
Konstanten werden syntaktisch analog zu Variablen deklariert. Auf der rechten Seite darf nur ein konstanter Ausdruck stehen. Konstanten sind können typisiert (typed) oder typenlos (untyped) sein. Typenlose Konstanten nehmen erst im Kontext ihrer Verwendung einen konkreten Typ an.
Eingebaute Konstanten in Go sind
-
nil
- der Null-Zeiger -
false
undtrue
, die Wahrheitswerte fürbool
Variablen -
iota
- ein Zähler für Konstanten, s.u.
Das folgende Beispiel vereint mehrere syntaktische Besonderheiten. Es
definiert die Konstanten Sonntag, Montag, …, Samstag zu den Zahlenwerten
0 bis 6 sowie anzahlTage
auf 7.
const (
Sonntag = iota
Montag
Dienstag
Mittwoch
Donnerstag
Freitag
Samstag
anzahlTage // nicht extern sichtbar
)
- Anstatt das Wort
const
vor jede Zeile zu schreiben, können mehrere Konstanten auch innerhalb einerconst ( ... )
Klammer definiert werden. Diese Klammerung ist auch mit Variablen oder Imports möglich. - Die eingebaute Konstante
iota
ist ein Zähler, der innerhalb einerconst ( ... )
Klammerung gilt. Er beginnt mit 0 und erhöht sich in jeder Zeile um 1. - Wenn hinter der Konstante (z.B.
Montag
) keine Zuweisung steht, wiederholt sich die Zuweisung der letzten Zeile. Somit werden in diesem Beispiel die Wochentage aufsteigend durchnummeriert. - Nur Konstantennamen, die mit einem Großbuchstaben beginnen, sind außerhalb des Packages sichtbar. Andere können nur innerhalb des selben Packages benutzt werden. Das gleiche gilt für Variablen, Funktionen und selbstdefinierte Typen.
var a int
var b *int // Zeiger auf int
a = 15
b = &a // b ist ein Zeiger auf a
*b = 20 // a ist nun 20
Ein Zeiger (pointer) ist eine Variable, die eine Referenz auf eine andere Variable
enthält. Im obigen Beispiel ist b
ein Zeiger auf die Variable a
. Zur
Deklaration des Zeigertyps und zur Dereferenzierung (Auswertung des
Objekts, auf das ein Zeiger zeigt), dienst der * Operator. Die Adresse
einer Variablen erhält man mit dem & Operator.
Go besitzt zwar Zeiger, aber keine Zeigerarithmetik; es ist also nicht möglich, durch Rechenoperationen einen Zeiger auf andere Objekte zu erhalten.
func foo() {
x := new(int) // Zeiger auf anonymes int
*x = 23
} // int wird durch Garbabe Collector gelöscht
Mit der eingebauten Funktion new()
wird ein neues, anonymes Objekt des
angegebenen Typs erstellt und ein Zeiger darauf zurück gegeben. Es gibt
keinen Variablennamen zu diesem Objekt, es ist nur über den Zeiger
erreichbar. Anders als z.B. in C++ können Objekte nicht explizit
gelöscht werden. Ein Go Programm erkennt automatisch, wenn es auf ein
Objekt keine Referenz mehr gibt; das Objekt wird dann durch die Garbage
Collection gelöscht.
Ein Zeiger in Go kann somit nur entweder nil
sein oder auf ein
gültiges Objekt zeigen. Die Dereferenzierung eines nil
Zeigers führt
zu einer Laufzeit-Fehlermeldung.
func mult(a, b int64) int64 {
return a * b
}
func add(a, b int64) (c int64) {
c = a + b
return
}
func main() {
x := mult(3, 8)
y := add(x, 10)
fmt.Println(x, y)
}
Funktionen werden mit dem Schlüsselwort func
eingeleitet. Danach folgt
der Funktionsname, in Klammern eine Liste der Parameter sowie den Typ
des Rückgabewertes. Wenn der Rückgabewert auch namentlich benannt wird
oder es mehrere Rückgabewerte gibt, müssen diese ebenfalls in Klammern
geschrieben werden.
func foo() (int, error) {
x := ...
return x, nil
}
func main() {
d, err := foo()
if err != nil {
panic(err)
}
...
}
Das angegebene Beispiel zeigt die gängige Methode der Fehlerbehandlung
in Go. Wenn die Funktion fehlerfrei ausgeführt wird, werden das Ergebnis
und nil
als Fehler zurückgegeben; ansonsten hat das Ergebnis einen
neutralen Wert und die Fehlervariable ist mit einem Wert besetzt, der
zum Typ error
(ein Interface, dazu mehr später) passt.
func main() {
k := func (i, j int) int {
return i * j
} (14, 9)
...
}
Funktionen können anonym, d.h. ohne Funktionsnamen, verwendet werden. Im Beispiel wird eine Funktion definiert und sogleich mit Parametern aufgerufen. Abgesehen vom fehlenden Namen erfolgt die Definition genauso wie oben beschrieben.
func summe(s ...float64) float64 {
sum := 0.0
for _, i := range s {
sum += i
}
return sum
}
func main() {
x := summe(1.5, 3, -4.0)
fmt.Println(x)
}
// auch erlaubt:
a := []float64{1.5, 3, -4.0}
y := summe(a...)
Funktionen können eine variable Anzahl von Parametern haben, wobei diese vom gleichen Typ sein müssen. Innerhalb der Funktion sind die Werte in einem Slice verfügbar (dazu später mehr). Umgekehrt kann ein Slice genutzt werden, um Parameter an eine Funktion mit variadischer Parameterliste zu übergeben.
func foo() {
lock(l)
defer unlock(l)
...
// unlock geschieht hier
}
Mit dem Schlüsselwort defer
kann die Ausführung einer Anweisung an das
Ende einer Funktion verschoben werden. Somit lassen sich
zusammengehörige Operationen (z.B. lock/unlock oder open/close) optisch
zusammen schreiben, obwohl sie im Kontrollfluß weit auseinander liegen.
defer
ist insbesondere praktisch, wenn mehrere Dinge geöffnet werden
(z.B. Netzwerkverbindung, File, Datenbankverbindung) und bei jedem davon
Fehler auftreten können.
// for-Schleife
summe := 0
for i := 1; i <= 10; i++ {
summe += i
}
// while Schleife
for ; i <= 10; { ... }
// oder
for i <= 10 { ... }
// Endlosschleife
for { ... }
Die for-Schleife setzt sich, wie bei vielen anderen Sprachen auch, aus einer Initialisierung, einer Endbedingung und einem Increment zusammen; die drei Teile werden durch Semikolon getrennt. Indem man den ersten und dritten Teil weg lässt, wird daraus eine while-Schleife. In diesem Fall können auch die beiden Semikolon entfallen. Eine for-Schleife ohne Endbedingung ist eine Endlosschleife.
Innerhalb der Schleife springt continue
zum nächsten
Schleifendurchlauf, break
verlässt die Schleife. Bei verschachtelten
Schleifen können Labels gesetzt werden, so dass mit break
auch die
äußere Schleife verlassen werden kann.
// if
if a < 0 { a = -a }
// if-else
if x := foo(); x < A {
return A
} else if x > B {
return B
} else {
return x
}
Es gibt keine eigenständige elsif
Anweisung. Statt dessen kann
else if
geschachtelt werden, geschweifte Klammern hinter else
können
entfallen.
// switch
switch m {
default: foo()
case 0, 2, 4, 6: bar()
case 1, 3, 5, 7: biz()
}
// Switch ohne Variable
switch {
case a < b: foo()
case a > c: bar()
case b == d: biz()
}
Bei der switch-Anweisung werden die Fälle (case) von oben nach unten
abgearbeitet. Die Position der default
-Bedingung ist egal; sie wird in
jedem Fall nur angesprungen, wenn sonst keiner der Fälle zutrifft.
Nach der Ausführung der Anweisungen hinter einem Fall wird die
Abarbeitung beendet. Es ist kein separater Ausstieg (wie z.B. break
in
C) nötig. Mit fallthrough
kann direkt in den nächsten Fall gesprungen
werden, auch wenn die Bedingung dort nicht zutrifft.
Eine Besonderheit ist die switch-Anweisung ohne Variable. Hier können
hinter den einzelnen case
Statements vollständige Vergleiche stehen,
die nicht notwendigerweise disjunkt sein müssen. Eigentlich ist das also
eine kürzere Schreibweise für if
Abfragen.
Eigene Datentypen werden mit type
definiert. Zur Definition eines
Verbundtyps dient struct
.
// Verbundtyp
type Person struct {
ID int
Vorname string
Nachname string
}
// Deklarationen
a := Person{1, "Harald", "Weidner"}
b := Person{ // ID bleibt 0
Vorname: "Erika",
Nachname: "Mustermann",
}
fmt.Println(a, b)
// ergibt:
// {1 Harald Weidner} {0 Erika Mustermann}
Eine Variable von einem Verbundtyp kann direkt initialisiert werden.
Dazu wird die Schreibweise Person{...}
genutzt. Innerhalb der
geschweiften Klammern werden entweder alle Felder in der Reihenfolge der
Typdefinition belegt, oder es wird jeweils der Feldname und -inhalt
angegeben. Im letzteren Fall können Felder weggelassen werden; sie
werden mit dem Defaultwert des jeweiligen Typs gefüllt (Null,
Leerstring, nil
oder false
).
Für Verbundtypen (oder andere selbst definierten Typen) können Methoden definiert werden.
// Methoden
func (p Person) FullName() string {
return p.Vorname + " " + p.Nachname
}
fmt.Println(a.FullName())
// ergibt: Harald Weidner
Eine Methode ist eine Funktion, die für einen bestimmten Typ definiert wird. Der Typ steht links des Funktionsnamens in Klammern. Aufgerufen wird die Methode mit der Syntax Variable.Methode().
In Go gibt es keine spezielle Notation einer Klasse. Methoden können mit (fast) allen selbst definierten Typen verwendet werden. Es gibt nur zwei Ausnahmen:
- Interface-Typen, also Typen der Art
type Foo interface { ... }
- Pointer-Typen, z.B.
type Pint *int
Interface-Typen können keine Methoden erhalten, da Interfaces zur Trennung von Spezifikation und Implementierung verwendet werden. Die Interfaces enhalten eine Spezifikation der Methoden, die vorhanden sein müssen; die konkreten Typen implementieren diese Methoden.
Pointer-Typen dürfen keine Methoden haben, da ansonsten die Regeln, welche Methoden zu einem Typ gehören, zu kompliziert würden. Pointer-Typen sind generell kein guter Stil und sollten vermieden werden.
func (p Person) String() string {
return fmt.Sprintf("[%d] %v %v", p.ID, p.Vorname, p.Nachname)
}
fmt.Println(b)
// ergibt:
// [0] Erika Mustermann
Manche Methoden haben eine spezielle Bedeutung für Funktionen der Go
Standardbibliothek. Ein Beispiel hierfür ist String()
. Wenn diese
Methode existiert und einen Rückgabewert vom Typ string
hat, dann wird
sie von den Ausgabefunktionen wie fmt.Println()
benutzt, um den Inhalt
der Variablen auszugeben.
// Interface
type FullNamer interface {
FullName() string
}
var x, y FullNamer
x = Person{23, "Hagbard", "Celine"} // erlaubt
y = int(3) // Fehler, int erfüllt interface nicht
Ein Interface besteht aus einer Liste von Methoden, die ein Typ besitzen muss, um das Interface zu erfüllen (“implementieren”). Ob ein Typ ein Interface erfüllt, wird nicht explizit angegeben, sondern ergibt sich implizit aus der Definition des Typs und seiner Methoden.
Das Interface wird selber als benutzerdefinierter Typ definiert. Es kann also Variablen vom Typ des Interfaces geben. Auf diese Weise wird Polymorphie in Go realisiert.
Ob ein Typ ein bestimmtes Interface erfüllt, kann zur Laufzeit geprüft werden. Von dieser Möglichkeit macht die Go Standardbibliothek häufig Gebrauch.
Es ist eine Konvention, dass ein Interface mit nur einer Methode den
Namen der Methode mit angehängtem “-er” bekommt. Ein Interface, dessen
einzige Funktion String()
heißt, erhält also den Namen Stringer
.
Das leere Interface interface{}
wird von allen Typen erfüllt. Einer
Variable vom Typ interface{}
können also Variablen jedes beliebigen
anderen Typs zugewiesen werden. So lassen sich Funktionen definieren,
die mit beliebigen Argumenten umgehen können. Bekanntestes Beispiel
dafür sie die Ausgabefunktionen wie fmt.Println()
, der man Variablen
und Konstanten aller Typen übergeben kann. Mittels der Reflection API
prüft die Funktion, welchen konkreten Typ ein Argument hat, und verhält
sich entsprechend. Das geschieht bei Bedarf auch rekursiv, z.B. bei
Verbundtypen oder Arrays.
// Arrays
var a [5]int64 // Array mit 5 Feldern
var b = [3]int{1, 2, 3} // entspricht [1, 2, 3]
var c = [...]int{1, 7, 4, 8, 3} // automatische Längenerkennung
Arrays sind statisch, d.h. in der Länge nicht änderbar. Die Inhalte können natürlich geändert werden.
// Slices
var s []int // uninitialisierter Slice
var t = make([]int32, 5) // Slice der Länge 5
var r = make([]int32, 5, 10) // Slice der Länge 5 und Kapazität 10
s = c[1:4] // s ist [7, 4, 8]
r = append(r, 9) // Anhängen eines Elementes, len(r) ist jetzt 6
t = append(t, 1) // Kapazitätserweiterung nötig, t wird umkopiert
Ein Slice ist eine Referenz auf ein (anonymes) Array oder einen Ausschnitt eines Arrays. Ein Slice besitzt eine Länge und eine Kapazität, die gleich oder größer der Länge ist. Die Länge kann verkleinert oder bis zum Erreichen der Kapazität vergrößert werden.
Die eingebaute Funktion append()
hängt Elemente an einen Slice an.
Wenn die Kapazitätsgrenze erreicht ist, erzeugt sie dabei einen neuen,
doppelt so großen Slice und kopiert die Elemente um.
// copy
s1 := []int{1, 2, 3, 4}
s2 := make([]int, 3)
copy(s2, s1)
fmt.Println(s2) // ergibt: [1 2 3]
Die eingebaute Funktion copy(dest, src)
kopiert einen Slice in einen anderen.
Es werden dabei höchstens so viele Elemente kopiert, dass die Länge des Ziel-Slice
nicht überschritten wird. Die Länge und Kapaztät des Slice, in den kopiert
wird, bleibt unverändert. Die Daten werden physisch kopiert, eine anschließende
Änderung des ursprünglichen Slice s1
ändert nicht die Inhalte von s2
und
umgekehrt.
Bei der Zuweisung eines Slice zu einem anderen (s2 = s1
) findet dagegen
keine Kopie der Inhalte statt. Beide Slices referenzieren anschließend die
selben Daten; nach einer Änderung des ursprünglichen Slice wie s1[0] = 5
ändert sich auch s2[0]
zu 5.
// Map
var Size map[string]float32
Size["Alice"] = 1.67
Size["Bob"] = 1.82
age := map[Person]uint {
Person{1, "Harald", "Weidner"}: 43,
Person{2, "Erika", "Mustermann"}: 29,
}
for k, v := range age {
fmt.Println(k, „=>“, v)
}
Eine Map ist ein assoziatives Array, vergleichbar mit dem Hash in Perl oder einem Dictionary in Python. Bei der Definition einer Map werden Schlüssel und Werte-Typ angegeben. Für den Schlüsseltyp muss es dabei eine Vergleichsfunktion (= bzw. !=) geben, damit entschieden werden kann, ob ein Schlüssel bereits vorhanden ist oder nicht. Ansonsten können beide Typen beliebig gewählt werden.
Das Keyword range
kann genutzt werden, um über alle Elemente einer Map
zu iterieren. Es kann auch für Arrays, Slices oder Strings verwendet werden.
// Goroutine
import (
"fmt"
"time"
)
func foo() {
fmt.Println("foo")
}
func bar() {
fmt.Println("bar")
}
func main() {
go foo()
go bar()
time.Sleep(time.Second)
}
Goroutinen sind nebenläufige Funktionen. Eine Goroutine wird wie eine
Funktion mit dem vorangestellten Schlüsselwort go
aufgerufen. Sie
terminiert am Ende der aufgerufenen Funktion, spätestens aber mit dem
Ende des Hauptprogrammes. Um den Effekt zu zeigen wartet in obigem Beispiel
das Hauptprogramm eine Sekunde.
Das Scheduling der Goroutinen wird von der Go Runtime übernommen. In der Regel werden viele Goroutinen auf wenige Betriebssystem-Threads verteilt. Dadurch sind Goroutinen sehr effizient. In der aktuellen Go Runtime benötigt eine Goroutine lediglich ein initiales Stacksegment von 2kB. Es ist problemlos möglich, mehrere Hundertausend Goroutinen nebenläufig zu betreiben.
import "runtime"
func main() {
runtime.GOMAXPROCS(16)
...
}
Um die Kapazitäten von Mehrkernprozessoren auszunutzen, muss die Runtime
angewiesen werden, Goroutinen auf mehrere Threads zu verteilen. Dazu
kann die Funktion runtime.GOMAXPROCS
verwendet werden. Alternativ ist
es möglich, die Environment-Variable GOMAXPROCS
zu setzen. Im Beispiel
wird die Runtime angewiesen, fix 16 Threads zu verwenden. Der Default ist,
so viele Threads zu verwenden, wie das System (virtuelle) CPU-Cores hat.
// Channel
func fib(c chan int) {
f0, f1 : = 0, 1
for {
c <- f0
f0, f1 = f1, f0+f1
}
}
func main() {
c := make(chan int)
go fib(c)
for i := 0; i < 25; i++ {
fmt.Println(<- c)
}
}
Ein Channel ist ein typisierter Transportkanal. Er kann bidirektional verwendet werden, um Daten des angegebenen Typs zu transportieren. Der Channel kann von allen Goroutinen und Funktionen verwendet werden, die ihn kennen. Typischerweise wird ein Channel im Hauptprogramm erzeugt und dann an die Goroutine als Argument übergeben.
// gepufferter Channel
c := make(chan int, 5)
Ein Channel kann gepuffert oder ungepuffert sein. Ein ungepufferter Channel blockiert den Schreiber, bis von ihm gelesen wird, oder den Leser, bis der Schreiber bereit zum Schreiben ist. Ein gepufferter Channel kann eine angegebene Anzahl von Variablen zwischenspeichern und somit als Pipeline genutzt werden.
// Prüfung auf geschlossenen Channel
val, ok := <- c
// ok == false wenn c geschlossen oder leer
Ein Channel kann mit close()
geschlossen werden. Beim Lesen kann
geprüft werden, ob der Channel bereits geschlossen wurde.
// Multiplexing
var c1, c2 chan int
var i1, i2 int
select {
case i1 = <- c1: { ... }
case i2 = <- c2: { ... }
default: { ... } // macht die Auswahl nichtblockierend
}
Eine Goroutine kann mit mehreren Channels arbeiten. Mittels einer
select
Anweisung bedient sie immer den Channel, der zum
Lesen/Schreiben bereit ist. Wenn mehrere Channel bereit sind, ist die
Reihenfolge undefiniert. Wenn keiner der Channel bereit ist, wird, falls
vorhanden, die default
Anweisung ausgeführt; ansonsten blockiert die
Anweisung, bis einer der Channels bereit wird.
// Goroutine kontrolliert beenden
package main
import (
"fmt"
"time"
)
func fib(c chan int) {
var f0, f1 int = 0, 1
for {
select {
case c <- f0: {
f0, f1 = f1, f0 + f1
}
case <- c: {
fmt.Println("fib() has ended")
return
}
}
}
}
func main() {
c := make(chan int)
go fib(c)
for i:=0; i<20; i++ {
fmt.Println(<- c)
}
c <- 0
time.Sleep(time.Second)
}
In diesem Beispiel kann das Hauptprogramm die Goroutine kontrolliert beenden. Dazu wird ausgenutzt, dass ein Channel bidirektional verwendet werden kann. Die Goroutine schreibt Fibonacci-Zahlen in den Channel, falls er dazu bereit ist, oder terminiert, wenn der Channel zum Lesen bereit ist. Das Hauptprogramm liest einige Werte und schreibt schließlich eine Zahl hinein, wodurch die Terminierung ausgelöst wird.
Reflection bezeichnet die Möglichkeit einer Programmiersprache, zur Laufzeit den Typ einer Variable zu ermitteln oder Eigenschaften des Typsystems zu prüfen.
Go bietet leistungsfähige Möglichkeiten der Reflection. Eine davon ist der Type Switch:
// Type switch
func foo(t interface{}) {
switch t := t.(type) {
default:
fmt.Printf("unexpected type %T\n", t)
case bool:
fmt.Printf("boolean %t\n", t)
case int:
fmt.Printf("integer %d\n", t)
case *bool:
fmt.Printf("pointer to boolean %t\n", *t)
case *int:
fmt.Printf("pointer to integer %d\n", *t)
}
}
Der Type Switch ist eine spezielle Form der switch - case
Anweisung.
Hier wird nicht der Inhalt, sondern der Typ einer Variablen zum
Kriterium gemacht. Die einzelnen Fälle sind skalare Typen des Typsystems
von Go.
Der Type Switch ist leistungsfähig, aber nicht hinreichend, um wirklich
beliebige Datenstrukturen zu erfassen. Wenn z.B. auch Verbundtypen
vorkommen können, muss zusätzlich auf andere Methoden aus der reflect
Bibliothek zurückgegriffen werden.
// explizite Interface-Prüfung
type Stringer interface {
String() string
}
func foo(a interface{}) {
if _, ok := a.(Stringer); ok {
fmt.Println("Methode String() vorhanden")
} else {
fmt.Println("Methode String() nicht vorhanden")
}
}
Eine andere, häufig genutzte Reflection Funktion ist die Prüfung, ob ein
Typ ein bestimmtes Interface erfüllt. Dazu kann die Schreibweise
Variable.(Interface) verwendet werden. Wenn ok
anschließend den Wert
true
hat, erfüllt der Typ das Interface, und die im Interface
definierten Methoden können aufgerufen werden.