Suche
Java to Go

Google Go für Java-Entwickler: Alles was Sie wissen müssen

Jan Stamer

© Shutterstock / LaurieSH

Docker, Kubernetes, Prometheus: Alle sind in Googles Programmiersprache Go geschrieben. Aber was macht Go interessant für Java-Entwickler? Go ist durch und durch auf Nebenläufigkeit und Parallelisierung ausgelegt. Außerdem hat Go ein statisches Typsystem und ist rasend schnell. Aber vor allem: Go macht Spaß.

Google hat die Programmiersprache Go als Nachfolger für C++ entwickelt. Dazu engagierten sie den Programmiersprachenforscher Rob Pike. Sein Auftrag: Entwickle für Google eine Programmiersprache, deren Code einfach zu warten ist und hoch performant bei der Ausführung. Herausgekommen ist Google Go. Aber Moment mal, ist denn Java kein würdiger C++-Nachfolger? Java ist hoch performant, keine Frage. Einfacher und wartbarer Code? Das ist sicher eine Frage des Geschmacks. Und über Geschmack lässt sich bekanntlich streiten. Google wollte aber eine Programmiersprache, mit der auch systemnah programmiert werden kann. Und da passte Java nicht so gut. Die Programmiersprache Go ist seit 2009 Open Source. Richtig bekannt geworden ist sie durch Docker, das in Go programmiert ist, wie viele andere Tools aus dem DevOps-Bereich, z. B. Kubernetes, Mesos oder Fleet. Der Programmiersprachenindex Tiobe wählte Go zur Programmiersprache des Jahres 2016.

Google Go wird häufig zur Netzwerkprogrammierung eingesetzt. Aber es wird mehr und mehr auch für die Entwicklung von Geschäftsanwendungen genutzt, oft in Form von Microservices. Es sind auch gar nicht so sehr die C++-Entwickler, die auf Go umsteigen. Nein, es sind Entwickler, die vorher mit Ruby, PHP und JavaScript gearbeitet haben. Aber warum? Warum nutzen die nicht Java? Schauen wir uns Go nun näher an und kommen später auf diese Frage zurück. Ein paar Fakten zu Go vorab: Go hat wie Java ein statisches Typsystem und einen Garbage Collector. Anders als Java läuft Go nicht in einer virtuellen Maschine ab, sondern wird als Binärdatei direkt auf der Maschine ausgeführt. Und Go ist nicht objektorientiert. Aber jetzt wollen wir Code sehen!

DevOps Docker Camp

Teilnehmer lernen die Konzepte von Docker und die darauf aufbauenden Infrastrukturen umfassend kennen. Schritt für Schritt bauen sie eine eigene Infrastruktur für und mit Docker auf.

Alle Termine des DevOps Docker Camps 2018 in der Übersicht

München: 19. – 21. Februar 2018
Berlin: 14. – 16. März 2018

Google Go: Hello Gopher!

Die Sprache Go hat ein Maskottchen, den Gopher (Abb. 1). Dem sagen wir in Go guten Tag. Dazu erstellen wir eine Datei hellogopher.go, die den folgenden Go-Code enthält:

 
package main

import "fmt"

func main() {
  fmt.Println("Hello Gopher!")
}

Schauen wir uns den Go-Code genauer an. Die Datei hellogopher.go enthält in der ersten Zeile die Package-Deklaration, wie in Java auch. In unserem Fall ist das Package main, ein spezielles Package, das der Einstiegspunkt eines Programms ist. Auf das Package folgen die importierten Packages, in diesem Fall das Package fmt aus der Go-Standardbibliothek. Die Datei enthält eine Funktion mit Namen main, die mit dem Schlüsselwort func beginnt. Diese main-Funktion entspricht der Main-Methode eines Java-Programms. Im Body der Funktion main wird die Funktion Println aus dem Package fmt mit dem Argument „Hello Gopher!“ aufgerufen. Semikolons sind in Go nicht notwendig.

Google Go - Gopher

Abb. 1: Der Gopher, das Go-Maskottchen

Nun führen wir das Programm aus. Dazu wird der Go-Code mit dem Befehl go build hellogopher.go in eine Binärdatei übersetzt, die auf der Maschine ausgeführt wird. Im aktuellen Verzeichnis liegt jetzt die Binärdatei hellogopher. Wir können sie mit dem Befehl ./hellogopher ausführen. Es gibt auch die Möglichkeit, mit dem Befehl go run hellogopher.go in einem Schritt zu kompilieren und auszuführen. Dateien mit Go-Code haben ein paar Besonderheiten verglichen mit Java-Dateien. Go-Dateien sind immer im Zeichensatz UTF-8 codiert, und der Quellcode ist mit dem Formatierungstool gofmt formatiert, das mit Go ausgeliefert wird. Eine Diskussion über Tabs versus Spaces kennen Go-Entwickler nicht. Go-Code ist immer gleich formatiert. Auch bei Package Imports und Variablen ist Go strikt. Der Go-Compiler duldet weder überflüssige Package Imports noch ungenutzte Variablen. Das sorgt dafür, dass der Code besser zu warten ist. Solche Dinge fallen bei der Java-Entwicklung erst durch eine statische Codeanalyse auf.

Lesen Sie auch: Google Go: Darum ist die Programmiersprache so beliebt

Ein erstes Go-Programm haben wir. Aber was ist die beste Entwicklungsumgebung für Go? Für Intellij und Eclipse gibt es Plug-ins für Go, JetBrains arbeitet an einer eigenen IDE für Go. Das ist auch sicher ein guter erster Schritt. Aber eigentlich braucht es gar keine so schwergewichtige Entwicklungsumgebung. Alles, was es für die Entwicklung von Go-Code braucht, gibt es als eigene Tools ganz nach der Unix-Philosophie. So gibt es goimport, um Imports aufzuräumen, gocode für Autovervollständigung, golint zur statischen Codeanalyse oder gorename, um Variablen umzubenennen. Gewissermaßen besteht also die Go-Entwicklungsumgebung selbst aus diversen Microservices. Werden diese Tools mit einem Editor kombiniert, ergibt das eine Art integrierte Entwicklungsumgebung. Dafür reicht auch ein schlanker Editor wie Vim, Emacs oder Atom.

Go funcy

Lagern wir nun die Begrüßung des Gophers in eine separate Funktion aus. Dazu erstellen wir eine Funktion mit dem Schlüsselwort func und nennen sie HelloGopher. Die Funktion nimmt keine Parameter, daher folgen auf den Namen HelloGopher leere Klammern. Der Body der Funktion steht in geschweiften Klammern. Die Funktion ist:

 
func HelloGopher() string {

   fmt.Println("Hello Gopher!")

}

Übergeben wir nun den Namen als Parameter. Dazu benennen wir die Funktion HelloGopher in Hello um. Wir erweitern die Funktion um einen Parameter name vom Typ string. Der Parameter wird, wie in Java, in die Parameterliste der Funktion geschrieben. In Go wird dagegen zuerst der Name des Parameters angegeben und danach dessen Typ. Die Signatur der Funktion ist nun also func Hello(name string). Im Body der Funktion übernehmen wir den Parameter in die Ausgabe der Funktion, also fmt.Println(„Hello “ + name + „!“). Den Parameter müssen wir noch beim Aufruf der Funktion übergeben, so wie auch in Java. Trennen wir jetzt noch die Erzeugung des Strings von dessen Ausgabe. Dazu lassen wir die Funktion den erzeugten String zurückgeben und geben ihn in der main-Funktion aus. Der Rückgabewert einer Funktion wird in Go hinter die Liste der Parameter geschrieben. Die Signatur ist also dann func Hello(name string) string. Im Body geben wir den String mit dem Schlüsselwert return zurück. Das folgende Listing zeigt den Code der Funktion mit Parameter und Rückgabewert. Funktionen können in Go mehrere Werte als Rückgabe haben, aber dazu später:

 

func Hello(name string) string {

   return "Hello " + name + "!"

}

Spätestens jetzt wäre es Zeit für einen Unit-Test unserer Funktion, mit dem Go-Test-Framework aus der Standardbibliothek. Das ist jedoch einem Unit-Test in Java sehr ähnlich, daher verzichten wir an dieser Stelle darauf, der Test ist aber im Git Repository zu diesem Artikel zu finden. In Go sind Funktionen Werte. Funktionen können als Parameter oder Rückgabewert verwendet werden oder wir können sie in einer Variablen speichern. Belassen wir es für diese erste Einführung in Go dabei, mehr dazu gibt es unter.

Variablen und Konstanten deklarieren

Variablen in Go müssen deklariert werden, bevor ein Wert zugewiesen werden kann. Variablen werden mit dem Schlüsselwert var deklariert. Zudem muss der Typ der Variable angegeben werden, also var s string. Nun können wir mit s = „Gopher“ einen Wert zuweisen. Deklaration und Zuweisung können auch in einer Anweisung var s := „Gopher“ zusammengefasst werden, dann muss der Typ nicht angegeben werden. Diese Notation werden wir im Folgenden verwenden. In Go gibt es auch Konstanten. Die werden mit Schlüsselwort const angegeben, dahinter folgen Typ und Wert, also const x string = „VAL“.

Sichtbarkeit: Privat oder exportiert?

Go kennt nur zwei Sichtbarkeiten, privat und exportiert. Privat heißt, nur innerhalb des Packages sichtbar, exportiert auch außerhalb. Fängt eine Variable oder Funktion mit einem Großbuchstaben an, ist sie exportiert, sonst privat. Unsere Funktion HelloGopher hat die Sichtbarkeit exportiert. Also kann HelloGopher auch aus anderen Packages aufgerufen werden.

Datentypen, Arrays und Slices in Go

In Go gibt es primitive Datentypen wie int, string, byte und so weiter, wie wir das von Java kennen. Aber insbesondere bei Zahlen gibt es viel mehr Varianten. Es gibt beispielsweise die Integer int8, int16, int32 jeweils mit und ohne Vorzeichen. In Go gibt es Arrays mit fixer Größe, wie in Java auch. Wird eine variable Länge benötigt, ist ein Slice die Alternative in Go zu der Liste, wie wir sie in Java kennen. Ein Slice wird mit der Funktion make erzeugt, als Parameter werden der Typ und die initiale Größe übergeben. Der Aufruf names := make([]string, 2) erzeugt also ein Slice mit Strings mit initialer Größe 2. Alternativ kann ein Slice auch direkt bei der Erzeugung mit Werten initialisiert werden mit names := []string{„Hans“, „Franz“}. Diese Syntax kennen wir aus Java. Auf Elemente eines Slice wird wie bei einem Array zugegriffen, beispielsweise names[0] für das erste Element. Anders als in Java, kann auch auf ganze Teile des Slice zugegriffen werden. So liefert names[1:3] die Elemente 1 bis 3 oder names[1:] alle Elemente ab dem ersten Element.

Kontrollstrukturen in Go

Es gibt in Go die Kontrollstrukturen if, for und switch. If funktioniert wie in Java, nur ohne Klammern. Die einzige Schleife in Go ist die for-Schleife. Die for-Schleife in Go kann wie eine for-Schleife in Java verwendet werden, mit Initialisierung, Bedingung und Inkrementierung, wie folgendes Listing zeigt:

 
for i := 1; i <= 10; i++ {
  fmt.Println(i)
}

Die for-Schleife in Go kann aber auch wie eine while-Schleife aus Java verwendet werden, indem nur die Bedingung hingeschrieben wird, wie in dem folgenden Listing. Eine Endlosschleife erhalten wir mit for {}:

 
i := 1
for i <= 10 {
  fmt.Println(i)
  i = i + 1
}

Um über Slices zu iterieren, wird die for-Schleife mit dem Schlüsselwort range zu einer Art foreach-Schleife. So können wir mit for i, name := range names {} über das definierte Slice mit Namen iterieren. Dabei enthält i den Index des Durchlaufs und name den Wert. Aber was, wenn wir den Index i gar nicht brauchen? Wir erinnern uns, der Go-Compiler meldet einen Fehler, wenn er ungenutzte Variablen findet. Wir können i durch _ ersetzen und teilen so dem Compiler mit, dass wir den Rückgabewert nicht benötigen. Ein switch-Statement gibt es auch, das ist nur eine kürzere Schreibweise für ifelseAbfolgen, also wie ein switch in Java mit break nach jedem case. Später werden wir auch noch das select-Statement kennen lernen. Aber dazu mehr, wenn es um Channels geht.

Struct: Java Bean ohne Logik

Go ist nicht objektorientiert. Aber Go kennt Structs, die den Objekten in Java am nächsten kommen. Ein Struct in Go ist so etwas wie ein Java Bean ohne Logik. Ein Struct hat einen Namen und enthält Member-Variablen, auf die mit der Punktnotation zugegriffen wird. Ein Struct für eine Person sieht wie im folgenden Listing aus:

 
 type Person struct {

   Name string

 }

Wir können nun mit p := Person{Name: „Guido“} ein neues Struct vom Typ Person erzeugen und der Variablen p zuweisen. Getter und Setter kennt Go nicht, den Wert des Members Name erhalten wir mit p.Name. Wir möchten gerne unsere Person mit p.greet() begrüßen. Geht das? Structs können doch keine Logik, also keine Funktionen, enthalten? Das stimmt, aber wir können eine Funktion schreiben, die auf dem Struct aufgerufen werden kann. Dazu schreiben wir eine Funktion greet, bei der auf das Schlüsselwort func der Empfänger der Funktion folgt. Der Receiver sieht aus wie eine Parameterliste mit einem Parameter, hat aber eine andere Bedeutung. Die Signatur der Funktion greet ist also func (p Person) greet(). Hat eine Funktion einen Receiver, kann sie auf dem Receiver mit der Punktnotation aufgerufen werden, also p.greet(), wie wir das wünschen:

 

 func (p Person) greet() {

   fmt.Println("Hello " + p.Name + "!")

 }

Structs sind keine Objekte. Structs sind einfache Datencontainer ohne Logik. Ein Struct kann nicht von anderen Structs erben, aber Structs können ineinander geschachtelt werden. Damit können viele Use Cases der Objektorientierung abgedeckt werden. Die Logik wird in Funktionen mit dem Struct als Receiver gekapselt. Funktionen ohne Receiver sind wie statische Funktionen in Java, Funktionen mit Receiver wie Methoden eines Objekts. In einem Package können nur Funktionen mit Structs des eigenen Packages als Receiver geschrieben werden. Structs anderer Packages können nicht erweitert werden. Das ist Absicht, weil es die Wartbarkeit des Codes beeinträchtigen würde.

Gibt es Referenzen in Go?

In Java arbeiten wir immer mit Referenzen, in Go ist das nicht so. Go übergibt eine Kopie des Werts und keine Referenz. Soll eine Referenz übergeben werden, ist das explizit anzugeben. Dazu gibt es das Konzept der Pointer. Angenommen, wir möchten innerhalb einer Funktion nameFritz den Namen einer Person auf Fritz ändern, die Person wird als Parameter übergeben. Die erste Implementierung ohne Pointer zeigt folgendes Listing:

 

 func nameFritz(p Person) {

   p.Name = "Fritz"

 }

Geben wir den Wert der Person vor und nach dem Aufruf von nameFritz aus, stellen wir fest, dass der Name der Person p sich nicht geändert hat. Warum? Der Funktion nameFritz wurde eine Kopie des Structs p übergeben. Der Name wurde nur in der Kopie auf Fritz gesetzt, nicht aber im ursprünglichen Struct. Wenn wir das möchten, müssen wir der Funktion nameFritz eine Referenz auf die Person p übergeben. In Go nennen wir das einen Pointer auf p. Dazu ändern wir den Aufruf nameFritz(p) in nameFritz(&p). Das & vor der Variable p sorgt dafür, dass ein Pointer auf p übergeben wird. Wir müssen auch noch den Typ des Parameters der Funktion nameFritz anpassen. Anstelle eines Structs Person erwartet sie nun einen Pointer auf ein Struct Person. Dazu wird * hinter den Typ geschrieben, der Parameter ist also p Person*. Den Body der Funktion müssen wir nicht anpassen. Die Funktion nameFritz mit Pointer zeigt dieses Listing:

 

func nameFritz(p *Person) {

  p.Name = "Fritz"

}

func main() {

  p := Person{Name: "Max"}

  nameFritzPointer(&p)

  fmt.Println("Hello " + p.Name)

}

Mit Pointern können wir ein Struct auch noch auf eine andere Art erzeugen, und zwar mit dem new-Operator. Wird ein Struct mit p := new(Person) erzeugt, hat Variable p den Typ Person*, also ein Pointer auf p, wie wir das aus Java kennen, nur dass wir es dort Referenz nennen. Die Arbeit mit Pointern ist als Java-Entwickler am Anfang ungewohnt, und so mancher wird sich an die C++-Zeit zurückerinnert fühlen. Aber mit der Zeit benutzt man die Pointer wie selbstverständlich.

Interfaces in Go

In Go gibt es Interfaces. Die Definition eines Interface beginnt mit dem Schlüsselwort type, es folgt der Name des Interface und danach das Schlüsselwort interface. Ein Interface enthält ausschließlich Methodensignaturen, wie in Java auch. Aber in einem Punkt unterscheiden sich Go und Java deutlich: In Go werden Interfaces nicht explizit implementiert, sondern implizit. Implementiert ein Typ alle Methoden des Interface, so implementiert er das Interface. Erstellen wir ein Interface für eine nette Person, nämlich eine, die grüßt. Das Interface heißt NicePerson und enthält die Methode greet(). Unser Struct Person erfüllt bereits das Interface, weil es bereits eine Methode greet() für das Struct Person gibt. Erstellen wir nun noch ein Struct Stepmother und dazu eine Funktion greet für das Struct Stepmother. Nun können wir eine Funktion passBy(p1 NicePerson, p2 NicePersion) erstellen, die unser NicePerson-Interface verwendet. Der Funktion passBy können wir als ersten Parameter eine Person übergeben und als zweiten ein Struct Stepmother. Beide implementieren das Interface NicePerson und grüßen also beim Vorbeigehen. Der Compiler prüft, ob der Aufruf zulässig ist. Dieses Listing zeigt die beiden Structs, das Interface und dessen Verwendung:

 

   type Person struct {

    Name string

   }


   type Stepmother struct{}


   func (p Stepmother) greet() {

    fmt.Println("Go to hell!")

   }


   func passBy(p1 NicePerson, p2 NicePerson) {

     p1.greet()

     p2.greet()

   }


  type NicePerson interface {

    greet()

   }

  func (p Person) greet() {

  fmt.Println("Hello " + p.Name + "!")

   }


   func main() {

    p := Person{Name: "Max"}

    s := Stepmother{}

    passBy(p, s)

   }

Da Interfaces implizit implementiert werden, ist es möglich, zuerst Structs zu erstellen und später Interfaces einzuführen, die von diesen Structs erfüllt werden, ohne die Structs selbst zu ändern. Es ist aber auch Vorsicht geboten, wenn Methods eines Structs oder eines Interface umbenannt werden. Auf jeden Fall unterscheiden sich Interfaces in Go grundlegend von Interfaces in Java. Interfaces werden in Go oft verwendet, auch für kleine Dinge. So gibt es z. B. das bekannte Interface Stringer, das nur die Methode String() string enthält und der toString()-Methode von Java-Objekten entspricht.

Wie werden Fehler behandelt?

Go kennt kein spezielles Konzept zur Fehlerbehandlung. Es gibt also weder Exceptions noch try-catch-Blöcke. Fehler sind ein Rückgabetyp einer Funktion, wie jeder andere auch. In der Go-Standardbibliothek gibt es einen Struct-Error, der für diesen Zweck vorgesehen ist. In Go kann eine Funktion mehrere Rückgabewerte haben. Eine Funktion, bei der ein Fehler auftreten kann, gibt üblicherweise als ersten Rückgabewert das Ergebnis der Funktion zurück und als zweiten Wert ein Struct vom Typ Error. Ist alles gut gegangen, ist das Error Struct leer, also nil, das Äquivalent für null in Java. Trat ein Fehler auf, enthält der Error weitere Informationen zum Fehler. Der Fehler kann nun behandelt oder weitergegeben werden. Mehr Infos zum Fehlerhandling in Go findet sich hier. Auch Laufzeitfehler gibt in Go, sie werden panic genannt. Das aber nur der Vollständigkeit halber.

Nebenläufigkeit ist Teil von Go

Go ist durch und durch auf Nebenläufigkeit und Parallelisierung ausgelegt. Nur so kann Go der Anforderung gerecht werden, auf moderner Hardware performant zu sein. Die Konzepte zu nebenläufiger Programmierung sind Teil der Programmiersprache Go und unterscheiden sich sehr von dem, was wir aus der Java-Welt kennen. Um in Go nebenläufig zu programmieren, gibt es zwei wichtige Konzepte, Goroutinen und Channels. Eine Goroutine ist ein leichtgewichtiger Thread, innerhalb dessen der Code immer sequenziell abläuft. Channels sind Nachrichtenkanäle, über die sich verschiedene Goroutinen Nachrichten schicken. Das theoretische Konzept hinter Channels und Goroutinen ist hier nachzulesen. Wie Threads in Java, können mehrere Goroutinen auf gleichen Speicherbereichen arbeiten. In Java geht das nicht anders, in Go sollte es die Ausnahme sein. Statt einen Speicherbereich zu teilen, sollten die Goroutinen Daten über Channels austauschen. Tauschen Goroutinen Daten aus, senden sie die Daten an einen Channel, auf dem eine andere Goroutine lauscht. Soweit zur Theorie, nun zur Praxis.

Jeder Funktionsaufruf wird mit dem Schlüsselwort go zur Goroutine. Um unsere erste Funktion HelloGopher in einer eigenen Goroutine auszuführen, reicht es also, den Aufruf der Funktion in der main-Methode in go HelloGopher() zu ändern. Allerdings sehen wir jetzt keine Ausgabe mehr. Warum? Auch die main-Methode unseres Programms ist eine Goroutine, die main-Goroutine. Ein Go-Programm endet, wenn die main-Goroutine endet. Da HelloGopher nun in einer eigenen Goroutine ausgeführt wird, ist in der main-Goroutine nichts mehr zu tun und sie endet. Damit wir die Ausgabe sehen, warten wir in der main-Goroutine, bis die Goroutine HelloGopher gelaufen ist. Dazu nutzen wir die WaitGroup aus dem Package sync. Wir erzeugen eine WaitGroup mit var wg sync.WaitGroup und teilen ihr mit wg.Add(1) mit, dass wir eine Goroutine ausführen wollen. Dann starten wir eine neue Goroutine, in der wir die WaitGroup mit defer wg.Done() über das Ende der Goroutine informieren und anschließend HelloGopher aufrufen. Dazu verwenden wir eine anonyme Funktion. Aber warum zuerst wg.Done() aufrufen? Achtung, das Schlüsselwort defer macht den Unterschied! In Java würden wir den Aufruf von wg.Done() in Java in einen finally Block packen, damit er immer ausgeführt wird, auch wenn ein Fehler auftritt. In Go erreichen wir das durch das Schlüsselwort defer. Mit defer wg.Done() wird wg.Done() nach der anonymen Funktion ausgeführt, auch bei Fehlern. Das folgende Listing zeigt den kompletten Code:

 

  func HelloGopher() {

   fmt.Println("Hello Gopher!")

  }
 

  func main() {

   var wg sync.WaitGroup

   wg.Add(1)

   go func() {

    defer wg.Done()

    HelloGopher()

   }()

    wg.Wait()

  }

Kombinieren wir das nun noch mit einem Channel. Wir erstellen einen Channel von Strings für die Namen von Personen mit names := make(chan string). Dann ändern wir die Funktion Hello in Hello(names chan string) und übergeben ihr als Parameter den Channel von Strings. Die Funktion wird aus der main-Goroutine wieder als eigene Goroutine gestartet mit go Hello(names). Nun fehlt noch das Senden und Empfangen der Namen. Dafür gibt es spezielle Channel-Operatoren. Mit dem Aufruf names <- „Jim“ senden wir den String Jim an den Channel. Innerhalb der Funktion Hello empfangen wir einen String mit name := <-names und speichern ihn in der Variable name. Das vollständige Programm gibt „Hello Jim!“ aus:

 

func Hello(names chan string) {

 name := <-names

 fmt.Println("Hello " + name + "!")

}


func main() {

 names := make(chan string)

 go Hello(names)

 names <- "Jim"

}

Wem ist aufgefallen, dass unser Codebeispiel mit dem Channel nun keine WaitGroup mehr enthält? Warum ist das nicht mehr notwendig? Die Goroutine, in der Hello ausgeführt wird, ist so lange blockiert, bis sie eine Nachricht auf dem Channel empfängt. Das ist aber erst der Fall, wenn die main-Goroutine die Nachricht Jim sendet. Dann läuft die Goroutine weiter und gibt Hello Jim!“ auf der Konsole aus. Die main-Goroutine ist nach dem Senden an den Channel fertig, also endet das Programm. Jeder Channel hat einen Buffer, das ist eine Queue von Nachrichten mit fester Größe. Nachrichten können nur an einen Channel gesendet werden, wenn in der Buffer Queue des Channels noch Platz ist. Ist kein Platz, wartet die sendende Goroutine, bis wieder Platz im Buffer frei ist. Bis dahin ist sie blockiert. Empfängt eine Goroutine eine Nachricht, so wartet sie, bis sie eine Nachricht aus der Buffer Queue erhält. Ist keine Nachricht in der Buffer Queue, ist die Goroutine blockiert, bis eine Nachricht kommt. In unserem Beispiel haben wir die Größe der Buffer Queue nicht explizit angegeben und somit wird sie mit der Größe 1 initialisiert.

In Go gibt es noch eine spezielle Kontrollstruktur für Channels, nämlich das select-Statement. Ein select-Statement sieht aus wie ein Switch, nur für Channels. Im case-Statement des selects wird eine Nachricht empfangen. Damit können wir die Funktion Hello unsereres Channel-Beispiels wie im folgenden Listing umschreiben.

 
 func Hello(names chan string) {

  select {

  case name := <-names:

     fmt.Println("Hello " + name + "!")

  }

 }

Das select-Statement hat nur einen Case. In diesem warten wir auf eine Nachricht aus dem names Channel. Das Verhalten ändert sich nicht. Wie zuvor wartet die Goroutine auf eine Nachricht des Channels und gibt dann „Hello Jim!“ aus. Das geht auch ohne select. Wozu denn dann select? Angenommen, wir wollen nicht ewig warten mit der Begrüßung, sondern maximal eine Sekunde. Was hat das mit select zu tun? Ganz einfach, das schreiben wir als case unseres select-Statements hin. Dazu erweitern wir das select-Statement um case <-time.After(time.Second). Das war’s.

Jetzt wartet das select-Statement maximal eine Sekunde auf eine Nachricht aus dem names Channel. Kommt keine Nachricht, wird der andere case ausgeführt und „No one here …“ ausgegeben. Das funktioniert, weil time.After(time.Second) einen Channel zurückgibt und wir dann auf eine Nachricht aus diesem Channel warten. Ein select-Statement kann auch verhindern, dass eine Goroutine blockiert wird. Dazu gibt es einen case default. Wird ein select-Statement ausgeführt, und auf keinem der Channels kann eine Nachricht empfangen werden, wird der Default Case ausgeführt. Damit kann die Goroutine nie dadurch blockiert werden, dass sie auf eine Nachricht aus einem der Channels wartet.

Java to Go?

Wir haben gelernt, wie Funktionen, Interfaces und Structs funktionieren und wie Goroutinen und Channels nebenläufiges Programmieren ermöglichen. An vielen Stellen wird deutlich, dass Go ganz anders funktioniert als Java. Statt Objekten gibt es Structs, Interfaces werden implizit implementiert, und statt Threads gibt es Goroutinen und Channels. Die Syntax von Go ist einfach zu lernen. Wer von Java kommt, muss vor allem bei einigen Konzepten umdenken. Das finde ich auch gut so, denn warum sonst sollten wir eine neue Programmiersprache lernen?

Was macht jetzt Go attraktiv für JavaScript-, Ruby- und PHP-Entwickler, attraktiver als Java? Go ist schnell und spart Ressourcen. Java gilt noch immer als behäbig, wenn auch oft zu unrecht. Java-Anwendungen sind sehr schnell, starten aber recht langsam und verbrauchen viel Speicher. Das ist bei Go anders. Go muss keine virtuelle Maschine hochfahren und kommt mit sehr wenig Hauptspeicher aus. Das ist vor allem für Cloud-Umgebungen interessant, wo Anwendungen schlafen gelegt werden und dann schnell hochfahren müssen und die nach Hauptspeicherverbrauch abgerechnet werden. Zum anderen hat Go eine moderne und mächtige Standardbibliothek, die viel abdeckt, was moderne Anwendungen brauchen. RESTful Services, JSON oder WebSockets sind mit drin, HTTP/2 wird seit Go 1.6 (Februar 2016) unterstützt. Java kann das auch alles – mit zusätzlichen Bibliotheken. Die braucht es in Go nicht.

Was macht Go für Java-Entwickler interessant? Go-Code ist einfach zu lesen und sehr gut zu warten. Go-Code darf langweilig sein, ja soll es sogar. Denn es soll nicht auf den Code an sich ankommen, sondern auf die Logik, die damit implementiert wird. Damit eignet sich Go für Enterprise-Anwendungen mit langer Lebensdauer, die durch viele Hände gehen. Außerdem eignet sich Go hervorragend für Anwendungen, die alle Power moderner Rechner ausnutzen. Und das sollte meiner Meinung nach jede Anwendung tun. Was haben wir davon, wenn die Anwendung vier Requests in vier Sekunden abarbeiten kann, aber ein einzelner Request auch vier Sekunden braucht? Was antworten wir, wenn der Product Owner fragt, wie wir diesen einen Request in einer Sekunde schaffen wollen, statt in vier? Brauchen Sie mehr Prozessoren, mehr Speicher? Nein, das hilft nicht. Ein Index in der Datenbank? Ernsthaft? Darauf müssen wir eine bessere Antwort haben. Und vielleicht ist Go ein Teil davon.

Mehr zum Thema:

Go 1.9: Das erwartet Entwickler in der neuen Version

Verwandte Themen:

Geschrieben von
Jan Stamer
Jan Stamer
Jan Stamer ist Senior-Softwareentwickler bei red6 in Hamburg, einem Spin-off der Hanse-Merkur-Versicherung. red6 entwickelt innovative Insurance-Lösungen mit zeitgemäßen Technologien mit Schwerpunkt auf Java und Node.js.
Kommentare
  1. 2017-07-20 14:18:56

    Deswegen sollten Java Entwickler nicht umsteigen -> Generics
    https://twitter.com/i/videos/tweet/883058510275149826

  2. skipperTux2017-07-26 21:04:08

    Im Code richtig, im Text falsch.

    "Dazu wird * hinter den Typ geschrieben, der Parameter ist also p Person* [...] Wird ein Struct mit p := new(Person) erzeugt, hat Variable p den Typ Person*, also ein Pointer auf p"

    Der * wird VOR den Typ geschrieben, nicht hinter. Siehe https://www.golang-book.com/books/intro/8

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.