Die Programmiersprache genauer betrachtet

Eine Einführung in Go

Eine Einführung in Go

Die Programmiersprache genauer betrachtet

Eine Einführung in Go


Unzufriedenheit ist der beste Motivator für Entwicklungstätigkeit. Die Sprache Go entstand, als Google-Mitarbeiter an die Grenzen der zur Verfügung stehenden Programmiersprachen Java und C++ stießen und nach einer besseren Alternative Ausschau hielten. Wir schauen uns die Sprache an, die vor allem im Hinblick auf skalierbare Netzwerkdienste und Cloud Computing entwickelt wurde.

Die neue Sprache sollte möglichst schnell zu direkten monetären Vorteilen führen. Diese Herangehensweise sorgte dafür, dass Go an Bekanntem und Vorhandenem ausgerichtet wurde. Das Produkt ist ein auf maximale Effizienz getrimmtes Arbeitswerkzeug, bei dessen Entwicklung die Bedürfnisse von produktiv arbeitenden Entwicklern im Mittelpunkt stehen.

Ein besonderes Feature ist die unter [1] spezifizierte lebenslange Kompatibilitätsgarantie. Das Entwicklerteam verpflichtet sich darin, für die gesamte Lebensdauer von Go 1.x Abwärtskompatibilität zu wahren: Es wird – anders als bei Ruby – auf absehbare Zeit keine Sprachänderungen geben, die „wohlverhaltenden“ Code betreffen.

Die ersten Schritte mit Go

Google bietet vorkompilierte Binärdateien für Windows, Mac OS und Linux an [2]. Aufgrund des Fokus auf Nebenläufigkeit ist es ratsam, erste Gehversuche auf einer mehrkernigen Maschine zu erledigen. Die folgenden Programmbeispiele entstanden auf einer vierkernigen AMD64-Workstation; als Betriebssystem kam Ubuntu 14.04 zum Einsatz.

Die Installation der Programmdateien ist nicht besonders komplex: Das Archiv wird an den von Google vorgeschlagenen Platz extrahiert, die Deklaration der unter [2] angeführten Pfad-Variable sorgt dafür, dass Skript und Co. die von Ihnen benötigten Werkzeuge ohne Probleme finden. Go funktioniert übrigens auch auf Einplatinencomputern mit ARM-Prozessor: Leider gibt es hier keine Binärdistribution, was das manuelle Kompilieren notwendig macht.

Leider ist das Bereitstellen der Compilerinfrastruktur nur die halbe Miete. Go setzt eine vergleichsweise komplexe Ordnerstruktur voraus. Wenn Ihr Code diesem Format nicht entspricht, so kommt es zu Compilerfehlern. Für die folgenden Beispiele erstellen Sie einen Verzeichnisbaum, der nach dem in Abbildung 1 gezeigten Schema aufgebaut ist.

Ein Go Workspace besteht aus drei Ordnern

Abb. 1: Ein Go Workspace besteht aus drei Ordnern

Der eigentliche Quellcode Ihrer Solution befindet sich – logischerweise – im Verzeichnis /src. Jedes Projekt-paket liegt in einer eigenen Ordnerstruktur, die auf Wunsch auch aus einem Git oder Mercurial Repository bestehen kann. Von go install kompilierte Binärdateien landen in /bin/, während /pkg/ für die Aufnahme von Bibliotheken und Ähnlichem zuständig ist.

Go setzt das Vorhandensein der GOPATH-Umgebungsvariablen voraus, die auf das zu verwendende Workspace-Verzeichnis verweist. Da man mitunter mit mehreren Workspaces arbeitet, ist es sinnvoll, die Variable nach folgendem Schema im Rahmen der Erstellung des Terminals zu deklarieren:

export GOPATH=/home/farhan/Schreibtisch/TamsHaus/GoWorkspace 

Aus Gründen der Bequemlichkeit fügen manche Programmierer das /bin-Verzeichnis zum Path hinzu. Dieser optionale Schritt erleichtert das Ausführen der Kompilate, ist aber nicht unbedingt notwendig.

Go sagt Hallo

Seit Jahrzehnten ist die Ausgabe von „Hello World“ am Bildschirm das Standardeinführungsbeispiel. Unsere Variante fällt etwas komplizierter aus, da sie einige Besonderheiten der Programmiersprache vorstellen soll. Erstellen Sie in einem Unterordner von /src/ eine Datei namens SUSSample1.go und versehen Sie sie mit dem Inhalt aus Listing 1.

Listing 1

package main 
 
import "fmt" 
 
func doSomething(howManyTimes int) { 
  for i := 0; i < howManyTimes; i = i + 1 { 
    fmt.Printf("Hello, world.\n") 
  }
}
 
func main() { 
  doSomething(22) 
}

Go wird mit einem lexikalischen Scanner (auch To­ken­izer oder Lexer genannt) ausgeliefert, der die in C und Java manuell einzugebenden Semikolons automatisch generiert. Das manuelle Eingeben eines Semikolons ist nur dann notwendig, wenn Sie das Ende eines Kommandos „explizit“ angeben wollen. Ein Beispiel dafür findet sich in der in doSomething implementierten for-Schleife.

Dieses Verhalten beeinflusst die Platzierung von geschwungenen Klammern. Sowohl in C als auch in Java ist die folgende Schreibweise einer Selektion – bis auf das Fehlen der in Go nicht vorgesehenen Klammern – legitim:

if i < f() 
{
    g()
}

Im Rahmen der Kompilation würde der Lexer nach dem Vergleich zwischen i und f ein Semikolon einfügen, was zu einem Compilerfehler führt. Die im Codebeispiel gezeigte Schreibweise mit dem Semikolon am „Ende“ des Parameters ist somit – nicht nur bei Funktionen – obligatorisch.

Deklarationen und mehr

Guido Krügers legendäres Lehrbuch zu C [3] enthielt eine Seite mit komplexen Deklarationen, deren Parsing auch erfahrene Entwickler vor arge Schwierigkeiten stellt. Go löst dieses Problem durch eine Anleihe bei Pascal: Der Variablenname kommt stets vor der Deklaration des Typs.

Zwecks Einsparung von unnötigen Eingabevorgängen wurden die in Pascal vorgesehenen Keywords samt dem Doppelpunkt ersatzlos gestrichen, was zur im Snippet demonstrierten Syntax führt:

x int
p *int
a [3]int

Pascal wird mit dem :=-Operator ausgeliefert, der für Zuweisungen zuständig ist. In Go ermöglicht er die Deklaration einer Variablen, deren Typ vom als Initialisierungswert verwendeten Parameter abhängig ist. Wir nutzen dies in der for-Schleife zur Erstellung der Laufvariable (for i := 0;). Go bleibt trotzdem eine streng typisierte Sprache. Der Lexer erkennt den Typ der zur Initialisierung verwendeten Konstante. Google spart dem Programmierer hier nur das explizite Eingeben des Typs.

In der Praxis führen die Optimierungen zu nicht unerheblichen Steigerungen der Lesbarkeit: Das bei C notwendige kreiselnde Interpretieren komplexerer Deklarationen entfällt [4].

Kompiliere mich

Komplexere Programme lassen sich durch Ausführung von go install kompilieren. Das Kommando kopiert das Resultat seiner Arbeit in den /bin-Ordner des Workspace. Da wir nur eine einzelne Datei erstellen müssen, ist das Overkill. Erfreulicherweise bietet der Go-Compiler mit build eine einfachere Variante an, die die Binärdatei im gerade aktuellen Verzeichnis ablegt:

farhan@FARHAN14:~/Schreibtisch/TamsHaus/GoWorkspace/src$ go build tamoggemon.com/susexample1/SUSExample1.go 

Go unterscheidet sich von anderen Programmiersprachen insofern, als die Spezifikation strenge Anweisungen über die Art der Formatierung enthält. Google liefert mit gofmt ein fertiges Werkzeug mit, das beliebigen Code in die kanonische Darstellung transformiert und das Resultat in die Kommandozeile ausgibt (Abb. 2).

„gofmt“ erstickt ­Flame Wars zum Thema Formatierung im Keim

Abb. 2: „gofmt“ erstickt ­Flame Wars zum Thema Formatierung im Keim

Mehrwertige Funktionen

Methoden sind ein elementares Mittel zur Zerlegung komplexer Programme. Die Einführung von Subs und Funcs war ein Quantensprung. Seit dem Pleistozän der Informatik wurde das Grundkonzept nicht angetastet. Eine Funktion ist und bleibt ein Befehl, der einen Rückgabewert anliefert. Structs und Arrays erlauben das Zurückgeben von mehreren Werten, setzen dabei aber zusätzlichen Code voraus.

Go unterstützt Funktionen mit mehreren Rückgabeparametern. Diese sind zur Meldung von Fehlern oder der Rückgabe von Zustandsinformationen ideal geeignet. Als Beispiel dafür wollen wir eine aus der Dokumentation entnommene Funktion ansehen, die Daten von einem Lesegerät herbeischafft (Listing 2).

Listing 2

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:]
  }
  return
}
n, e= ReadFull(anR, aBuf)

Das Schlüsselwort func kann zwei in Klammern zu setzende Parameterlisten entgegennehmen. Die erste deklariert die in die Funktion einzuführenden Werte, während die zweite Liste die an den Aufrufer zurückzugebenden Variablen festlegt.

In praktischem Go-Code stolpert man immer wieder über eine Optimierung. Sie ist immer dann sinnvoll, wenn die zurückzugebenden Werte immer „en bloque“ anfallen – das Benennen der Ausgabeparameter entfällt in diesem Fall ersatzlos:

func nextInt(b []byte, i int) (int, int) {
. . .
 return x, i
}

Slices und ihre Funktionen

Arrays sind in Go-Datentypen „erster Klasse“. Das Übergeben eines Arrays an eine Funktion versorgt den Methodenrumpf mit einer Kopie des Stapelspeichers. Dieses Verhalten ist in mehrerlei Hinsicht subideal: Kopieroperationen sorgen für zusätzlichen Overhead; eventuell beabsichtigte Kommunikation zwischen zwei Methoden wird erschwert.

Der Adressoperator erlaubt das Übergeben eines Zeigers auf ein typisiertes Array. Diese aus C bekannte Vorgehensweise ist in Go nicht sonderlich beliebt. Die idiomatisch korrekte Vorgehensweise besteht darin, die Methode zur Verarbeitung eines Slices zu befähigen.

Die Rolle eines Slices lässt sich am einfachsten verstehen, wenn wir seine Implementierung betrachten. Der Datentyp besteht aus einem Headerobjekt, das einen Zeiger auf den zu verwendenden Datenspeicher und zwei Indexvariablen zur Beschreibung des interessanten Bereichs enthält. Unter [5] findet sich folgende Strukturdeklaration, die nur von akademischer Bedeutung ist – es ist nicht möglich, den Header eines Slices direkt anzusprechen:

type SliceHeader struct {
  Data uintptr
  Len  int
  Cap  int
}

Im Rahmen der Deklaration unterscheiden sich Slices nur insofern von normalen Arrays, als die eckigen Klammern leer bleiben. Wir wollen uns dies anhand eines kleinen Beispiels ansehen, das den Inhalt eines Arrays mit anderen Elementen überschreibt:

func processString(workOnMe []int) { 
  for i:=0; i<len(workOnMe); i=i+1{ 
    workOnMe[i]=22; 
  }
}

workOnMe wird in processString ohne Größenangabe deklariert. Dies genügt, um die Funktion für den Interpreter als Slice-Verarbeiter zu kennzeichnen: Arrays entstehen in Go nur dann, wenn der Entwickler eine Kombination aus Datentyp und Größe angibt.

main erstellt ein neues Array, das im nächsten Schritt durch println in die Kommandozeile ausgegeben wird. Die eckige Klammer hinter dem Namen des Arrays weist Go dazu an, ein Slice zu erstellen, das den gesamten Inhalt des Felds umfasst. Durch den zweiten Aufruf von println kann das Ergebnis dann kontrolliert werden:

func main() {
  b := [5]int{1, 2, 3, 4, 5}
 
  fmt.Println(b)
  processString(b[:])
  fmt.Println(b)
}

Das wunschgemäße Verhalten des Demoprogramms lässt sich durch Ausführung beweisen. Der Inhalt des Arrays wurde von processString vollständig ersetzt:

farhan@FARHAN14:~/Schreibtisch/TamsHaus/GoWorkspace/src$ ./SUSDemo
[1 2 3 4 5] 
[22 22 22 22 22] 

Es ist nicht unbedingt notwendig, ganze Arrays an Slice-verarbeitende Funktionen zu übergeben. Der Slice-Operator erlaubt das Spezifizieren von Anfangs- und Endpunkten; die in Go verbaute Methode make erstellt neue Slices samt dem dazugehörenden Speicherungsarray.

Wir wollen uns dies anhand einer kleinen Veränderung des Beispiels in Listing 3 ansehen, die printf mit verschiedenen Slice-Typen versorgt.

Listing 3

func main() {
  b := [5]int{1, 2, 3, 4, 5}
 
  fmt.Println(b[:])
  fmt.Println(b[2:])
  fmt.Println(b[:3])
  fmt.Println(b[2:4])
}

Wie beim vorigen Beispiel gilt auch hier, dass das Verhalten des Programms nur anhand seiner Kommandozeilenausgabe sinnvoll nachvollziehbar ist:

farhan@FARHAN14:~/Schreibtisch/TamsHaus/GoWorkspace/src$ ./SUSExample1 [1 2 3 4 5] 
[3 4 5] 
[1 2 3] 
[3 4] 

An dieser Stelle sei ein Hinweis auf den in Go integrierten Garbage Collector erlaubt. Die Programmiersprache trägt nicht mehr benötigten Speicher – ähnlich wie Java – von Zeit zu Zeit automatisch ab. Es ist mit Hausmitteln nicht möglich, ein Element zur sofortigen Zerstörung freizugeben.

Was ist ein String?

C stellt Zeichenketten als char-Array dar. Diese numerisch einfache Vorgehensweise erleichtert die Verarbeitung der enthaltenen Daten, kann aber im Zusammenspiel mit Konstanten unangenehme Nebeneffekte verursachen. Go umgeht dieses Problem durch die Feststellung, dass ein String – per se – unveränderlich ist.

Dies hat insofern interessante praktische Implikationen, als ein aus einem String erstelltes Slice ja normal ansprechbar ist. Zur Klärung dieser Frage verwenden wir ein kleines Testprogramm, das einen mit einer Konstante belebten String in ein Slice umwandelt und dann einige Zeichen austauscht (Listing 4).

Listing 4

func main() {
  s:= "Teststring"
  fmt.Println(s)
  c := []byte(s)
  c[2] = 'B' 
  fmt.Println(string(c))
  fmt.Println(s)
}

Wer das Programm in der Kommandozeile ausführt, erhält folgende Ausgabe:

farhan@FARHAN14:~/Schreibtisch/TamsHaus/GoWorkspace/src$ ./SUSDemo
Teststring 
TeBtstring 
Teststring 

Witzigerweise funktioniert dieses Programm nur dann, wenn Sie den String in ein Slice konvertieren. Beim im Snippet gezeigten direkten Zugriff kommt es während der Kompilation zu einem Fehler der Bauart „cannot assign“:

func main() {
  s := "Test" 
  fmt.Println(s) 
  s[0] = 'h' 
  fmt.Println(s) 
}

Bei der Erstellung von mehrsprachigen Programmen muss die Unicode-Fähigkeit der Sprache berücksichtigt werden. Unser oben gezeigtes Slice wird im Fall von „exotischeren“ Zeichen die einzelnen Unicode-Bytes zurückliefern – weitere Informationen dazu finden Sie unter [6].

Traps und Leaves

Symbian-Programmierer hassten ihr Betriebssystem aufgrund einer ärgerlichen Besonderheit: Das Fehlschlagen mancher Methoden führte zur Auslösung eines Leaves. Dieses Exception-artige Unwesen hatte die unangenehme Eigenschaft, den Stack durch „rückwärtslaufende“ Programmausführung „schrittweise“ zu zerlegen. Seine destruktive Wirkung ließ sich nur durch einen Trap-Aufruf anhalten.

Go implementiert mit panic/recover ein ähnliches System. Wir wollen es anhand eines kleinen Beispiels ansehen, das seine Verwendung illustriert:

func stirb() {
  defer fmt.Println("deferred aus Stirb")
  defer fmt.Println("deferred aus Stirb, zur Zweiten")
  fmt.Println("Stirb lässt grüßen")
  panic("Ich sterbe")
}

stirb() illustriert die Nutzung von zwei neuen Konstrukten. Erstens erlaubt das defer-Kommando das Festlegen von Aktionen, die erst nach der Abarbeitung des Methodenkörpers zum Einsatz kommen. Die dort eingegebenen Befehle werden auch dann ausgeführt, wenn die Methode durch Aufruf von return oder durch eine auftretende Panik beseitigt wird.

Die panic-Methode dient zum Auslösen eines derartigen Panikzustands. Sie nimmt einen Parameter entgegen, der zur weiteren Beschreibung des Fehlers eingesetzt werden kann. Paniken beenden die Ausführung aller gerade aktiven Routinen, führen dabei aber die durch defer eingeschriebenen Methoden fertig aus.

main beginnt diesmal mit der Festlegung eines Error Handlers, der durch defer in den Stack geschoben wird. Der darauffolgende Aufruf von stirb() sorgt für die Auslösung einer Panik, die durch die in func befindliche recover-Anweisung abgefangen wird:

func main() {
  defer func() { 
    if err := recover(); err != nil { 
      fmt.Println("Fehler", err) 
    } 
  }() 
  stirb() 
}

Go unterscheidet sich von klassischen TRAP/LEAVE-Systemen insofern, als die durch Aufruf von recover anzulegende Trap in Go-Programmen erst nach der Auslösung des Fehlers passiert; recover liefert zudem den an panic übergebenen Fehlercode zurück, der so zur Weiterverarbeitung des Programmzustands genutzt werden kann.

Zum Verständnis der Funktion des Gesamtprogramms ist es sinnvoll, einen Screenshot der in Abbildung 3 gezeigten Ausgabe zur Hand zu haben. Beachten Sie, dass defer mehrere Kommandos nach dem FIFO-Prinzip abarbeitet.

Panic, Resume und Defer sorgen für seltsame Resultate

Abb. 3: Panic, Resume und Defer sorgen für seltsame Resultate

Achten Sie bei der Realisierung nebenläufiger Programme stets darauf, dass das Unwinding des Stacks nur im in Panik verfallenden Thread erfolgt. Wenn sein Stack verbraucht ist, bevor ein recover()-Punkt erreicht ist, so wird der gesamte Prozess von der Runtime terminiert.

Selektionen in smart

Anfänger ärgern sich oft über den vergleichsweise „armen“ Funktionsumfang von Switch: C++ beschränkt Entwickler auf die Verarbeitung von Integers. Go nutzt diesen häufigen Kritikpunkt für eine komplette Umgestaltung der Selektionen und Iterationen: C- und Java-Programmierer müssen auf einige liebgewonnene Konstrukte verzichten.

Als kleine Entschädigung dafür gibt es eine wesentlich verbesserte Switch-Struktur, die nun sogar vollwertige Ausdrücke verarbeiten kann (Listing 5).

Listing 5

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
}

unhex() wurde aus der Dokumentation übernommen: Es gilt als üblich und idiomatisch, komplexe if-else-Bäume über ein Switch-Statement abzubilden. Das Fehlen von break-Statements ist übrigens kein Fehler. Von Haus aus wird jedes neue Case-Tag als implizites Break interpretiert.

Unnötige Codeduplizierung lässt sich durch das Aneinanderreihen mehrerer Bedingungen durch Kommata umgehen:

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

Falls Sie unbedingt einen Fallthrough im Stil von C oder Java benötigen, so müssen Sie dies durch das explizite Angeben des Kommandos fallthrough anweisen. Ein weiterer wichtiger Unterschied betrifft den Aufbau der Schleifen: Die do- bzw. while-Schleifen werden durch Abarten von for ersetzt:

// Like a C for
for init; condition; post { }
 
// Like a C while
for condition { }
 
// Like a C for(;;)
for { }

Go erleichtert den Zusammenbau der Bedingungen durch Veränderungen an der Operatorpräzedenz. Kennern von C++ fällt bei der Betrachtung der im Snippet gezeigten Prioritätentabelle mit Sicherheit der eine oder andere Unterschied auf:

Go operator precedence:
1. *   /   %  <<  >>  &  &^
2. +   -   |  ^
3. ==  !=  <  <=  >   >=
4. &&
5. ||

Die Steigerung der Priorität von Shift-Operatoren ist ein Entgegenkommen an all jene, die Go zur Realisierung von hardwarenahem Code einsetzen (Stichwort x<<8 + y<<16). Beachten Sie zudem, dass die unären Operatoren ++ und nur in Statements zum Einsatz kommen dürfen.

Es geht ohne Objekte

Der Siegeszug der objektorientierten Programmierung hat dafür gesorgt, dass die meisten Informatiker heute mit Hat-Ein- und Ist-Ein-Beziehungen arbeiten. Go verlangt an dieser Stelle radikales Umdenken: Konzepte wie Klassen und Vererbungen sind der Sprache nicht bekannt. Google arbeitet stattdessen mit im C#-Stil erweiterten Strukturen, die zudem ein oder mehrere Interfaces implementieren können.

Wir wollen uns dies anhand eines Beispielinterface ansehen. Rocket spezifiziert die zur Realisierung eines Torpedos notwendigen Basismethoden; die Funktion fire() ist für das Auslösen eines Raketenstarts zuständig:

type rocketInterface interface{
  Fire() 
  getName() string
}
 
func fireRocket(rocket rocketInterface){
 
  rocket.Fire()
}

Die eigentliche Realisierung einer Rakete erfolgt dann nach folgendem Schema:

type bisnovat struct{
  myName string 
}
 
func (me bisnovat) Fire(){
  fmt.Println("Bisnovat Abgefeuert") 
}
 
func (me bisnovat) getName() string{
  return me.myName 
}

Go kennt keinen „Implements“-Operator. Ein Typ gilt dann als kompatibel, wenn er alle im Interface deklarierten Funktionen enthält: Es ist theoretisch möglich, ein Interface auf einem Fremdtypen aufzubauen.

Methodendeklarationen unterscheiden sich in Go nur insofern von normalen Funktionen, als sie ein als Receiver bezeichnetes Zusatzelement bekommen. Es handelt sich dabei um einen Zeiger, der auf die für den Aufruf der Funktion zuständige „Objektinstanz“ verweist. Der Methodenkörper kann ihn während der Laufzeit benutzen, um das aufrufende Element zu manipulieren. Im Rahmen der Kompilation dient der Receiver zur Deklaration des Typs, der mit der Methode dotiert werden muss.

Der für die Erstellung der Objekte zuständige Code ist insofern interessant, als er zwei verschiedene Initialisierungsmethoden vorführt:

func main() {
 
  r := bisnovat{} 
  fireRocket(r) 
  pR := new (bisnovat) 
  fireRocket(*pR) 
}

Geschwungene Klammern leiten in Go die Konstruktion einer „normalen“ Struct-Instanz ein: Die Klammern können eine Werteliste umschließen, die in die neu generierte Instanz wandern. Der new-Operator erstellt ebenfalls eine neue Instanz, liefert aber stattdessen einen Pointer zurück. Dieser lässt sich durch den *-Operator „deadressieren“, erlaubt aber – per Definition – keine weitergehende Pointerarithmetik.

Interfaceimplementierungen lassen sich verschachteln und sogar zur Erweiterung von eingebauten Typen einspannen. Weitere Informationen zu diesen Möglichkeiten finden Sie im Interfaces-Abschnitt von [7].

Threading

Erfahrene Informatikausbilder geben unter der Hand zu, dass der durchschnittliche Programmierer mit der Erstellung von anspruchsvollen nebenläufigen Produkten überfordert ist. Neben durch die Ausbildung verursachten Missverständnissen liegt dies auch daran, dass die Arbeit mit Mutex und Co. nicht unbedingt zu den einfacheren Teilgebieten der Informatik gehört.

Go umgeht den Problembereich durch die Einführung eines neuen und vergleichsweise radikalen Programmierparadigmas. Die als goroutine bezeichneten „Threads“ unterscheiden sich von in anderen Sprachen implementierten Nebenläufigkeitskonstrukten insofern, als sie nicht immer 1:1 auf Betriebssystemthreads umgelegt werden. Es kann sogar sein, dass mehrere Threads gemeinsam eine Goroutine bearbeiten [8].

Noch gravierender ist, dass das Teilen von Speicher in Go-Programmen unerwünscht ist. Die Kommunikation zwischen den einzelnen goroutines erfolgt sodann über so genannte Channels, die wie eine Art Stream funktionieren.

Als Beispiel dafür realisieren wir ein Programm, das die von zwei als Endlosschleife arbeitenden Goroutines angelieferten Informationen ausgibt (Listing 6).

Listing 6

func datenQuelle(i int, ch chan int){
  for{ 
    ch <- i 
  }
}
 
func main() {
 
  chanA:=make(chan int) 
  chanB:=make(chan int) 
  go datenQuelle(1,chanA) 
  go datenQuelle(2,chanB) 
 
  for{ 
    fmt.Println(<-chanA) 
    fmt.Println(<-chanB) 
  }
}

goroutines sind im Grunde genommen normale Funktionen, deren Aufruf durch den Befehl go angestoßen wird. Neue Channels entstehen durch Aufruf von make(). Die hier verwendete ungepufferte Variante hat die unangenehme Eigenschaft, die Programmausführung solange zu pausieren, bis sowohl ein Lese- als auch ein Schreibzugriff erfolgt. Der daraus entstehende Deadlock sorgt dafür, dass die beiden Routinen immer „nacheinander“ triggern.

Mutexe finden sich in der hier aus Platzgründen nicht weiter besprechbaren Standardbibliothek der Sprache. Go bietet mit dem Select-Befehl zudem syntaktischen Zucker an, der bei der Kommunikation hilft. Weitere Informationen dazu finden Sie unter [9].

Includes effizient verwalten

C/C++-Programme haben die unangenehme Eigenschaft, während der Kompilation zu „wachsen“. Ein zehn Megabyte großes Projekt kann – nach der Auflösung aller Includes – schon mal zehn Gigabyte Daten in Richtung des Präprozessors geschoben haben. Dass ein Gutteil davon aus mehrfach geladenen Headerdateien besteht, folgt aus der Logik.

Im Rahmen der Entwicklung von Go wurde besonderes Augenmerk auf eine möglichst effiziente Verwaltung von Includes gelegt. Der wichtigste Unterschied zu C++ ist, dass das Inkludieren eines nicht benötigten Elements zu einem Compilerfehler führt: Der Abhängigkeitsbaum eines Go-Projekts muss stets „sauber“ bleiben.

Mehr lernen

Es gibt kaum eine Programmiersprache, die in einem Artikel eines Fachmagazins vollständig beschreibbar ist. Während die Auslassungen bei akademischen Sprachen verschmerzbar sind, fiel mir die Längenbeschränkung diesmal besonders schwer. Go enthält zu viele Funktionen, die Aufmerksamkeit verdienen und im praktischen Einsatz Zeit sparen.

Mit C++ erfahrene Entwickler sollten im ersten Schritt die offizielle Anleitung für Umsteiger lesen [7]. Das vergleichsweise kompakte Dokument zeigt eine Vielzahl interessanter Unterschiede auf, nutzt dabei aber schon Bekanntes. Effective Go sticht aus der in [10] zugänglichen Dokumentation heraus. Auch hier bekommen umsteigewillige Entwickler eine kurzgefasste Tour über wichtige Aspekte der Sprache. Spezifische Probleme werden in Go by Example erklärt [11].

Freunde kompletter Lehrbücher können das unter [12] einsehbare Go-Book durcharbeiten. Das Werk geht auf die hier aus Platzgründen nicht besprochene Standardbibliothek ein. Fortgeschrittenere Themen werden in diversen Lehrbüchern besprochen: Dank der Unterstützung durch Google gibt es mittlerweile ein breit gefächertes Sortiment von englischsprachiger Literatur.

Fazit

Go frustriert Umsteiger aufgrund seiner Ähnlichkeit zu C und Java. Wer sich mit Ruby oder Lisp beschäftigt, geht von vorneherein davon aus, nur wenig existierendes Wissen weiterverwenden zu können. Die Syntax von Go wirkt auf den ersten Blick vertraut – die während der Umstellung auftretenden Compilerfehler und der damit einhergehende Verlust von „Vertrautem“ sind dann doppelt schmerzhaft.

Nach der Überwindung dieser Umstellungsschwierigkeiten erweist sich Go als effizientes Werkzeug zur Steigerung der Produktivität. Die wesentlich schnellere Kompilation großer Codebasen ist dabei nur die halbe Miete. Wer weniger Steuerzeichen eingeben muss, spart sich Zeit.

Mehr zu Go

Gopher

Cloud-native entwickeln mit Go

Go ist die Sprache der Cloud: 14 von 19 Projekten der Cloud Native Computing Foundation im höchsten Reifegrad nutzen Go, darunter Kubernetes und Docker. Go ist blitzschnell, hat starke Typen und Go-Routinen, die all Ihre 64 Cores unter Dampf setzen. Sie lernen in diesem Artikel am Beispiel des Tarifrechners DogOP mit Go Cloud-native zu entwickeln. Also Fallschirm anziehen, jetzt gehts in die Wolken!


Links & Literatur

[1] http://golang.org/doc/go1compat

[2] https://golang.org/doc/install

[3] Krüger, Guido: „Go to C-Programmierung“, Addison-Wesley, 2007

[4] http://blog.golang.org/gos-declaration-syntax

[5] http://golang.org/pkg/reflect/#SliceHeader

[6] http://blog.golang.org/strings

[7] https://github.com/golang/go/wiki/GoForCPPProgrammers

[8] http://stackoverflow.com/questions/24599645/how-do-goroutines-work-or-goroutines-and-os-threads-relation

[9] http://www.golang-book.com/10/index.htm

[10] https://golang.org/doc/

[11] https://gobyexample.com/

[12] http://www.golang-book.com

Tam Hanna

Tam Hanna befasst sich seit der Zeit des Palm IIIc mit Programmierung und Anwendung von Handcomputern. Er entwickelt Programme für diverse Plattformen, betreibt Onlinenewsdienste zum Thema und steht für Fragen, Trainings und Vorträge gern zur Verfügung.


Weitere Artikel zu diesem Thema