Die Golumne

Go – die C-volution von Google

Frank Müller

© Reneé French (CC BY 3.0 DE) ©SuS Media

In der Welt der Programmiersprachen ist Googles Spross Go ein noch recht junger Vertreter. Doch in den acht Jahren seit ihrer Vorstellung erreichte die Sprache immer mehr Entwickler. War ihr Einsatz anfangs noch auf die Cloud und Systemprogramme fokussiert, so findet man Go nun zunehmend auch in anderen Anwendungsbereichen wieder. In den bekannten Statistiken von Tiobe, GitHub, Stack Overflow, RedMonkIEEE oder dem PYPL-Index rückt Go kontinuierlich Platz um Platz vorwärts. Ist Google damit ein guter, wenn nicht gar „der“ C-Nachfolger gelungen? Eine kurze Vorstellung der Sprache bildet den Auftakt zu unserer neuen Kolumne.

Die ersten Schritte in der Welt der Go-Programme lassen schnell den Eindruck entstehen, es sei eigentlich doch nur C, so groß sind die Ähnlichkeiten zwischen den beiden Programmiersprachen. Zudem enthält das ursprüngliche Entwicklerteam mit Rob Pike, Ken Thompson und Robert Griesemer auch entsprechende Größen. Rob Pike und Ken Thompson waren als Entwickler der Bell Labs Mitglieder des Unix-Teams, Thompson entwickelte zudem B als Vorgängersprache von C. Griesemers Background waren die Java Hot-Spot VM und Googles V8 JavaScript Engine. Und diese alten Hasen hatten nun das Ziel, ein besseres C mit dem Wissen von heute zu entwickeln. Die neue Sprache sollte in den Strukturen und Schlüsselworten einfacher sein, um die Lesbarkeit und damit auch die Wartbarkeit zu verbessern. Eine zusätzliche Vereinfachung ist die Verwaltung des Speichers via Garbage Collection. Weitere Ziele waren eine hohe Performanz und die Unterstützung moderner Systeme mit Binärprogrammen. Hierfür unterstützt Go verschiedene Rechnerarchitekturen und beinhaltet auch Nebenläufigkeit. Diese leichtgewichtige Form der zeitgleichen Codeausführung sorgt bereits auf Single-Core-Systemen für leistungsfähige und elegante Softwarearchitekturen. Zudem skaliert sie hervorragend auf Multi-Core-Computern, wie sie heute üblich sind.

Die Nähe zur Sprache C ist Google Go gerade in der Anfangszeit oft vorgehalten worden. Sie ist im Wesentlichen imperativ. Elemente der Objektorientierung oder der zunehmend wieder an Popularität gewinnenden funktionalen Programmierung sind nicht sofort erkenntlich. Generics und Exceptions sind nicht vorhanden. Dafür bietet die Sprache allerdings einigen Komfort durch eine übersichtliche Anzahl gut handzuhabender Strukturen, durch eine implizite Bestimmung von Typen, durch den einfachen Export von Bezeichnern über eine Großschreibung sowie durch ein wunderbar einfaches und flexibles Interfacekonzept.

Gopher ante portas: Go hält Einzug auf JAXenter!

Wenige (relativ) neue Programmiersprachen erfreuen sich derzeit einer so großen Belibtheit wie Go. Googles Alternative für „C“ ist immer häufiger Zentrum interessanter Anwendungen und mittlerweile im Mainstream angekommen. Grund genug, sich ein wenig eingehender mit der Sprache zu befassen. Folgende Artikel, Kolumnen und Infografiken sind Teil unserer aktuellen Go-Aktion auf JAXenter:

Happy Gophing!

Go Infografik: Die wichtigsten Sprachkonstrukte auf einen Blick

Neben diesen grundlegenden und schnell erlernbaren Sprachelementen bringt Go eine gut ausgestattete Standardbibliothek mit, wobei diese nicht auf grafische Oberflächen ausgelegt ist. Vielmehr finden sich hierin sehr viele sogenannte Packages zur Systemprogrammierung wieder, beispielsweise für I/O, Dateisysteme, Netzwerke und Kryptografie. Und auch für die Entwicklung von Webanwendungen ist gut gesorgt: HTTP-Multiplexing und Templates helfen bei der unkomplizierten Implementierung handlicher und autonomer Programme mit einem Webinterface.

Der dritte starke Block sind die Tools. In der Regel kommt der Entwickler nur mit go als dem mit diversen Unterkommandos ausgestatteten Programm in Kontakt. go build kompiliert die Quellen begeisternd schnell, go get installiert externe Packages und go test führt Unit-Tests aus. Dazu kommen noch viele mehr. Anfangs waren viele der neuen Nutzer von go fmt überrascht. Mit diesem Tool werden die Quellen in ein standardisiertes Format gebracht, was einigen anfangs sauer aufstieß. Hierzu sind ja noch die Glaubenskämpfe zu Klammerpositionen in anderen Sprachen bekannt, jedoch wurde der Go-Community zum Glück sehr schnell klar, wie stark die einheitliche Formatierung die Zusammenarbeit im Team oder in Open-Source-Projekten erleichtert.

Gesunder Pragmatismus

Der übliche Einstieg in die Programmiersprache mit „Hello, World!“ erscheint in der Tat etwas langweilig.

package main

func main() {
  hello := "Hello, World!"
  println(h)
}

Die Software in Go ist in Paketen organisiert. Ein package net aus der Systembibliothek wird von nutzenden Paketen via import net importiert. Eine besondere Rolle hat dabei das Paket main, das in ein ausführbares Programm kompiliert wird. Den Einstieg bildet die Funktion main(). Im Beispiel wird darin mit println() der String ausgegeben. Erste auffällige Vereinfachung ist schon der Verzicht auf das Semikolon als Ende einer Anweisung. Es existiert, ist jedoch nur als Separator mehrerer Anweisungen innerhalb einer Zeile notwendig. Eine weitere Vereinfachung ist die implizite Bestimmung von Typen. Hier kommt, wie oben zu sehen ist, die Zuweisung über := zum Einsatz. Als weiteres Beispiel sei eine Funktion func Add(a, b int) int { … } definiert, ihr Rückgabewert ist vom Typ int. Dessen Nutzung kann nun explizit oder implizit erfolgen:

// Explizit.
var i int

i = Add(47, 11)

// Implizit.
j := Add(20, 17)
k := i – j

Hier erlaubt der Zuweisungsoperator := eine gleichzeitige Deklaration und Initialisierung von Variablen. Damit gewinnt Go an vielen Stellen den Komfort dynamisch typisierter Sprachen wie Python, ohne die Sicherheit statisch typisierter Sprachen zu verlieren. Weiterhin fällt die Großschreibung von Add() auf. In Packages werden derartige Bezeichner automatisch exportiert, Kleinschreibung beschränkt die Sichtbarkeit auf das jeweilige Paket.

Mit den Kontrollkonstrukten verhält es sich ähnlich einfach wie schon mit den impliziten Variablen. Eine bedingte Anweisung mit if hat immer die Form:

if expression {
  ...
}

oder

if expression {
  ...
} else {
  ...
}

Hier wird die Bedingung immer ohne Klammern und der auszuführende Block selbst bei nur einer Anweisung in geschweifte Klammern gesetzt. Das macht das Leben leichter. Für die häufige Verbindung aus einer Anweisung und einer Entscheidung gibt es noch die verkürzte kombinierte Form:

if out = doSomething(in); out > 42 {
...
}

Auch bei den Schleifen haben die Entwickler der Sprache auf eine möglichst große Vereinfachung geachtet. Diese kennen nur das Schlüsselwort for, dafür aber in vier Formen. Traditionell für Zähler ist es das for i := 0; i < 42; i++ { … }. Hier zeigt sich erneut einer der Vorteile der impliziten Typenbestimmung mit :=. Für das Iterieren über Datenmengen wird das for in Verbindung mit dem Schlüsselwort range eingesetzt. Bei for i, v := range a { … } über Arrays und Slices ist i der Index und v der dazugehörige Wert. Bei Maps stehen mit for k, v := range m { … } in k der Schlüssel und in v der Wert zur Verfügung. Zu guter Letzt ergibt for v := range ch { … } sequenziell die Werte aus einem Channel. Die hier kurz erwähnten Typen stelle ich im Folgenden noch vor. Bleiben noch zwei weitere Formen der Schleifen: Einmal die reine, an eine Bedingung gebundene Schleife wie for v < 42 { … } oder die einfache Endlosschleife mit for { … }. Letztere kommt insbesondere mit der Nebenläufigkeit oft zum Einsatz.

In meinen Programmen möchte ich natürlich nicht nur mit den einfachen Typen arbeiten. Diese werden mir von Go reichhaltig geboten: Boolean, Strings, Ganz- und Gleitkommazahlen in verschiedenen Größen, auch komplexe Zahlen sind dabei. Doch oft benötige ich auch Strukturen mit oder ohne Methoden, Arrays von Typen, Hashmaps und mehr. Diese können sehr einfach über das Schlüsselwort type definiert werden. Bei einer Struktur verwundert die Form noch nicht:

type Inventory struct {
  ArticleID string
  Quantity  int
}

Typen können aber auch direkt auf den Basistypen basieren:

type ArticleID string

type Inventory struct {
  ArticleID ArticleID
  Quantity  int
}

In der direkten Zuweisung von Werten zeigt sich kein Unterschied, aber in der Verwendung von Variablen:

func GetQuantity(id ArticleID) int { ... }

var idA ArticleID = "123"
var idB string = "456"

// Klappt.
qA := GetQuantity(idA)

// Kompiliert nicht.
qB := GetQuantity(idB)

Soweit stellt sich Go sehr imperativ dar. Eine Objektorientierung, wie von vielen Sprachen gewohnt, bietet Googles Schöpfung nicht. Dies heißt aber nicht, dass Go keine Methoden kennt. So lässt sich für die ArticleID zum Beispiel func (id ArticleID) IsValid() bool { … } definieren, sodass ein if idA.IsValid() { … } möglich ist. Doch der Spaß mit den Methoden lässt sich durch Interfaces noch viel weiter führen. Ein einfaches Interface mit der eben definierten Methode schaut dann wie folgt aus:

type Validator interface {
  IsValid() bool
}

Schön ist dabei, dass Validator durch jeden Typ implementiert wird, der die entsprechende Methode aufweist, auch ohne dies explizit zu deklarieren. Diese Freiheit erlaubt es auch, ein Interface erst dort zu definieren, wo es benötigt wird. Möchte ich beispielsweise eine Funktion implementieren, die aus einem Slice von Variablen die gültigen herausfiltert, so folgt auf das oben angegebene Interface die in Listing 1 dargestellte Funktion. Diese kann nun mit jeglichen Variablen, die die Methode IsValid() implementieren, aufgerufen werden. Was diese Gültigkeit nun individuell bedeutet und wie sie implementiert wird, ist für diesen generischen Filter irrelevant. Bei …Validator handelt es sich übrigens um ein variadisches Argument, es ist als letztes Argument einer Funktion erlaubt. So kann die Funktion mit AllValids(va), AllValids(va, vb, vc) oder auch AllValids(all…) aufgerufen werden, wenn es sich bei all – wie schon in der Funktion – um einen Slice von Validator handelt.

func AllValids(validators ...Validator) []Validator {
  var all []Validator
  for _, v := range validators {
    if v.IsValid() {
      all = append(all, v)
    }
  }
  return all
}

Doch wie implementiere ich nun eine Methode? Im Prinzip ebenso wie eine Funktion, nur mit einem Receiver Type. Dieser wird benötigt, um die Methode einem Typ zuzuordnen (Listing 2).

type Fortytwo int

func (f Fortytwo) IsValid() bool {
  return f == 42
}

type SumFortytwo struct {
  a int
  b int
}

func (s *SumFortytwo) IsValid() bool {
  return s.a + s.b == 42
}

Eine spezielle Typdefinition, beispielsweise class, ist nicht notwendig. Einfache und komplexe Typen können Methoden implementieren und somit auch Interfaces realisieren. Was hierbei auffällt, ist der * bei SumFortytwo. Die Motivation ist hier, dass der Receiver als Referenz betrachtet wird und Änderungen von einfachen oder Feldwerten aus einer Methode heraus möglich sind. Ohne den Asterisk (Sternchen) handelt es sich um Wertetypen, Go unterscheidet hier im Gegensatz zu Sprachen mit ausschließlichen Referenztypen. Anfangs etwas ungewöhnlich, gewöhnt man sich dann jedoch schnell daran. In einer der folgenden Kolumnen werde ich noch näher darauf eingehen.

Nebeneinander miteinander

War bisher alles nur ein Vergleich bekannter Konstrukte mit der entsprechenden Syntax in Go, so ist die Nebenläufigkeit etwas Besonderes. Bei der Arbeit mit Threads behindert ihr Overhead, ihr „Gewicht“, den schnellen Wechsel. Der Datenaustausch findet in der Regel via Shared Memory und Mutexes statt. Dem entgegen sind Goroutines in Go leichtgewichtiger und kommunizieren über Channels miteinander. Mutexes stehen zwar auch zur Verfügung, denn nicht immer lohnen sich Goroutines, jedoch ist das Zusammenspiel zwischen Goroutines und Channels das eigentliche Highlight. Der deutlich schnellere Kontextwechsel und ein geringerer Ressourcenverbrauch als bei den Threads dienen hierbei nicht einer Parallelisierung der Arbeit, auch wenn diese ebenfalls möglich ist. Wichtiger ist hingegen die Nutzung der Goroutines als Mittel zur Strukturierung einer Applikation.

Oftmals werden Goroutines nicht als einfache lineare Aufgaben gestartet, die im Hintergrund ihre Arbeit verrichten und sich dann brav beenden. Stattdessen werkeln sie in der Regel mit einer internen Endlosschleife, die Daten aus einem oder mehreren Channels intern sequenziell verarbeitet. Somit wird ein konkurrierender Zugriff auf die Variablen im Verantwortungsbereich einer Goroutine vermieden, die Zugriffe sind isoliert. Sind die Verantwortlichkeiten für Zustände auf diese Art sauber geklärt, so können unterschiedliche Goroutines problemlos nebeneinander und miteinander werkeln. Die Vorstellung des Zusammenwirkens vieler spezialisierter Mitarbeiter in einem Unternehmen hilft hierbei sehr.

Prinzipiell ist eine Goroutine nichts anderes als eine Funktion oder Methode, die mit dem Schlüsselwort go zum Arbeiten in den Hintergrund geschickt wird. Dort verhält sie sich wie jede andere Funktion auch, hat also auch den gleichen Zugriff auf ihren äußeren Scope. Einen Handle oder eine ID zur äußeren Kontrolle oder Überwachung wie in Erlang/OTP gibt der Aufruf von go nicht zurück. Hierfür gibt es aber Bibliotheken oder einfache Ansätze als Ersatz.

Für die Kommunikation mit Goroutinen sind die bereits erwähnten Channels verantwortlich. Eine Goroutine schreibt in sie, eine andere liest aus ihr. Es können aber auch viele Sender und ein Leser sein, ein Sender und viele Leser oder viele Sender und viele Leser. Sie transportieren, synchronisieren und erleichtern den Aufbau flexibler Architekturen. Die Channels selbst sind typisiert. Ein Channel für Integer kann so also keine Strings übertragen. Normal arbeiten sie synchron, ein Schreibvorgang blockiert bis zum Auslesevorgang. Doch es sind auch Puffer möglich, bei denen die Blockade erst bei einem vollen Zustand einsetzt. Ein ganz einfaches erstes Beispiel sei ein Druckservice (Listing 3).

func startPrinter() chan string {
  c := make(chan string)
  count := 0
  go func() {
    for s := range c {
      count++
      fmt.Println(count, ") ", s)
    }
  }()
  return c
}

func main() {
  c := startPrinter()
  
  c <- "Hello, World!"
  c <- "It works ..."
  
  close(c)
}

Was passiert hier? Die Funktion startPrinter() erzeugt einen Channel für Strings sowie einen Zähler. Dann wird eine anonyme Funktion als Goroutine gestartet. Sie liest in einer Schleife Strings über den erzeugten Channel, zählt den Zähler hoch und gibt den String in der Form 1) Mein String auf dem Bildschirm aus. Unabhängig davon, welche Goroutine wann in den Channel schreibt, wird der Zähler sauber sequenziell hochgezählt. Nachdem die Goroutine in den Hintergrund geschickt wurde, liefert startPrinter() den Channel zurück.

Die Hauptfunktion des Programms ist nicht sehr aufregend. Mit dem Start des Printers erhalten wir den Channel, in den dann mit dem Operator <- zwei Strings zum Ausdruck geschickt werden. Das Schließen des Channels mit close(c) beendet die Schleife innerhalb der Goroutine. Auch sie selbst stoppt danach. Dies Beispiel ist natürlich noch nicht gerade aufregend, deutet aber die Einfachheit des Themas Nebenläufigkeit sowie deren Konstrukte in der Sprache Go an.

Fazit

Ich hoffe, ich konnte mit meiner kleinen Reise in Googles boomende Sprache Ihre Neugierde wecken. Es war nur ein erster Einblick in eine Sprache, die ihre Verwandtschaft mit den weiteren Sprachen der C-Familie nicht leugnen kann. Und ebenso lässt sich leicht erkennen, dass das Team um Robert Griesemer, Rob Pike und Ken Thompson viel Wert auf eine Vereinfachung sowie auf die Nutzung moderner Mehrkernsysteme gelegt hat. In den weiteren Folgen stelle ich viele weitere Dinge rund um Go vor. Sei es die Bibliothek, die Toolchain (mit dem extrem schnellen Compiler und der Portabilität) oder der Aufbau typischer Weblösungen beziehungsweise nebenläufiger Backends mit pragmatischen Lösungsansätzen. Es gibt genug zu berichten, seien Sie gespannt.

Verwandte Themen:

Geschrieben von
Frank Müller
Frank Müller
Der Oldenburger Frank Müller ist seit über dreißig Jahren in der IT zu Hause und im Netz vielfach als @themue anzutreffen. Sein Geld verdient er als Go-Entwickler bei der Status Research & Development. Das Interesse an Googles Sprache begann 2009 und führte inzwischen zu einem Buch sowie mehreren Artikeln und Vorträgen zum Thema.
Kommentare

Hinterlasse einen Kommentar

2 Kommentare auf "Go – die C-volution von Google"

avatar
400
  Subscribe  
Benachrichtige mich zu:
Anonym
Gast

hello := „Hello, World!“
println(h)

Muss das nicht println(hello) heißen?

Entwickler
Gast

Spätestens seit Graal, AOT, CDS, AppCDS, nativen Images und den in Graal verfolgten polyglotten Ansatz in Java, lassen mein Interesse an neuen Programmiersprachen rapide schwinden.
Sollte es trotzdem mal hardwarenah sein, leisten C/C++ ausgezeichnete Dienste.