Das Wissensportal für IT-Professionals. Entdecke die Tiefe und Breite unseres IT-Contents in exklusiven Themenchannels und Magazinmarken.

SIGS DATACOM GmbH

Lindlaustraße 2c, 53842 Troisdorf

Tel: +49 (0)2241/2341-100

kundenservice@sigs-datacom.de

Warum Go? – Teil 1: Tour de Force

Nach einem kurzen Blick auf Googles Sprache Go mag sich so mancher fragen, wozu noch eine Sprache. Es findet sich eigentlich nichts Besonderes, kein einzigartig esoterisches Konstrukt, geradezu langweilig. Und dennoch findet Go Zuspruch, gerade im Bereich der systemnahen Programme und Backends. Ein Blick auf Infrastruktur und Sprache soll die Gründe hierfür beleuchten.
Author Image
Frank Müller

Author


  • 25.07.2019
  • Lesezeit: 17 Minuten
  • 88 Views

Die Geschichte von Go begann Ende 2007 basierend auf den Erfahrungen der Entwickler Rob Pike, Ken Thompson und Robert Griesemer. Die ersten beiden sind bereits seit Langem aus dem Unix-Umfeld bekannt, haben an Plan 9 mitgewirkt und UTF-8 entwickelt. Die Historie von Robert Griesemer umfasst Strongtalk, die Java HotSpot VM und die JavaScript-Engine V8.

Die drei Entwickler wollten eine Sprache mit der Sicherheit und Stabilität statisch kompilierter Sprachen sowie der Ausdrucksstärke und Bequemlichkeit dynamisch typisierter Interpretersprachen schaffen. Einsatzraum sollten moderne skalierbare Systeme werden. Und so startete die Entwicklung Mitte 2008 und führte zu einer ersten öffentlichen Vorversion im November 2009. Go 1.0 erschien dann im März 2012 und erhielt 2014 sein Maskottchen – das Gopher. Entworfen wurde es von Renée French, der Ehefrau von Rob Pike. Inzwischen hat Go den Stand 1.12 erreicht, 1.13 kündigt sich schon an. Die Hauptversionsnummer 1 verdeutlicht hierbei den Anspruch der Sprachkompatibilität, doch die Diskussionen zu einer Version 2 und dem Weg dorthin sind bereits im Gange.

Let’s go

Die Installation bringt im Wesentlichen neben Quellen und kompilierten Bibliotheken drei Binärdateien mit sich. Neben godoc für die Erzeugung einer HTML-Dokumentation aus den Quellen und für eine einheitliche Formatierung der Quellen ist dies nur das Tool go. Dieses eine Programm ist der Compiler, Linker, Generator, Tester, Installateur und mehr. Und ebenso gestaltet sich das Ergebnis eines Compiler-Laufs. Im Falle eines Programms wird nur ein Binary erzeugt, welches die komplette Laufzeitumgebung und die benötigten Bestandteile der importierten Bibliotheken enthält. Es lässt sich so einfach durch Kopieren auf anderen Systemen installieren, dank Cross-Compiling sogar über Rechnerarchitekturen hinweg. Schon diese Eigenschaften gefallen im Umfeld der Systemprogrammierung.

Auf mit der Sprache C vertraute Entwickler wirkt Go sofort vertraut. Die zur Verfügung stehenden Konstrukte sind überschaubar: Pakete zur Organisation, hierin Funktionen und auch Typen mit Methoden. Zur Abstraktion der Methoden lassen sich Interfaces definieren. Dem Code stehen eine Reihe von einfachen und von zusammengesetzten Datentypen zur Verfügung, dazu kommen Kontrollkonstrukte für Verzweigungen und Schleifen. Bis auf die Methoden und Interfaces bisher für imperative Sprachen nichts Besonderes. Und auch diese überraschen keinen Kenner objektorientierter Sprachen. Dazu kommen noch Zutaten aus den funktionalen Sprachen, da Funktionen in Go ebenfalls Typen sein dürfen.

Der Mix zeigt schon, wie wenig Wert Go darauf legt, in die Schublade eines Paradigmas gelegt zu werden. Ein einfacher und pragmatischer Ansatz sollte es sein.

Doch eine Besonderheit kommt noch dazu: die Nebenläufigkeit. Funktionen werden in Form leichtgewichtiger Goroutinen auf einen Thread-Pool verteilt, mehrere Tausend gleichzeitig sind kein Problem. Sie können individuelle Aufgaben erledigen oder kontinuierlich über typisierte Channel Daten für die Verarbeitung sowie deren Ergebnisse austauschen.

Kurzeinstieg

Erste Anweisung einer Go-Datei ist die Package-Anweisung. Sie benennt das jeweilige Paket, welches mehrere Quelldateien enthalten darf. Einzig fest definierter Name ist main, welcher zu einem kompilierten Programm mit dem Namen des Verzeichnisses führt (s. Listing 1).

package main
func main() {
println("Hello, World!")
}
Listing 1: Einstiegspunkt

Die Packages können auch hierarchisch geschachtelt werden, was bei größeren Bibliotheken sinnvoll ist. In der Regel gleichen sich dabei die Namen von Verzeichnis und Package. Jedoch enthalten die Verzeichnisse normal auch Dateien mit dem Namensmuster
<name>_test.go für Komponententests. Man kann anhand des Namens zwischen Black- und White-Box-Tests unterscheiden. Zum Beispiel handelt es sich bei package foo um einen White-Box-Test, bei package foo_test hingegen um einen Black-Box-Test, welcher das originale Paket importieren muss. In beiden Fällen finden sich die Tests nicht in den kompilierten Binärdateien für Programme und Bibliotheken.

Wie angedeutet erfolgt die Nutzung von Paketen durch ihren Import. Die Anweisung hierfür erfolgt nach der Package-Anweisung. Die Packages der Standardbibliothek werden dabei nur über ihre jeweiligen Paketnamen angesprochen. Der Name externer Bibliotheken setzt sich aus Repository-Server, Repository und Paketname zusammen. Dieser Name muss zur Nutzung dem exportierten Bezeichner vorangestellt werden. So werden Konflikte der Namensräume zumindest vermindert (s. Listing 2).

package foo
import (
"fmt"
"github.com/moby/moby/volume"
)
func main() {
fmt.Printf("Default driver is %v\n", volume.DefaultDriverName)
}
Listing 2: Import von Paketen

Die Installation der externen Pakete erfolgt implizit oder durch das Kommando go get <paketname>. Bestandteile der Infrastruktur helfen beim Umgang mit eigenen Repository-Servern und der Vermeidung von Versionskonflikten. Letzteres findet allerdings erst derzeit seinen Weg in die Welt der existierenden Bibliotheken. Der Export von Konstanten, Funktionen, Typen oder globalen Variablen erfolgt ohne Schlüsselworte.
Go unterscheidet nur zwischen package private und global. Nicht zu exportierende Bezeichner werden kleingeschrieben, zu exportierende groß. Und so wundert es nicht, dass in Listing 2 die Funktion Printf() und die Konstante DefaultDriverName mit einem Großbuchstaben beginnen. Die im Package fmt für den internen Gebrauch definierten Konstanten ldigits und udigits sind hingegen außerhalb nicht sichtbar.

Die Funktion main() zeigt bereits die Definition von Funktionen in Go. Sie werden durch das Schlüsselwort func eingeleitet. Sie können keine oder bis zu einer variablen Anzahl von Argumenten enthalten. Gleichzeitig können sie kein, ein oder mehrere Werte zurückgeben. Letzteres ist insbesondere in der Fehlerbehandlung in Go wichtig.

In Listing 3 nimmt die Funktion Add() eine beliebige Anzahl an Ganzen Zahlen entgegen, um diese zu addieren. Bei Div() verhält es sich für eine Division entsprechend, allerdings wird hier geprüft, ob einer der Divisoren 0 ist, und in diesem Fall ein Fehler ausgegeben. Der in beiden Fällen verwendete Typ int ist vorzeichenbehaftet und richtet sich nach der Rechnerarchitektur, heute also oftmals 64 Bit. Ohne Vorzeichen bietet Go den uint und für eine genauere Definition der Größe int8/uint8, int16/uint16, int32/uint32 und int64/uint64. Für den Typ uint8 ist noch das byte als Alias definiert, für int32 die rune, also 4 Bytes für jedes Unicode-Zeichen. Weitere numerische Typen sind float32 und float64 sowie complex64 und complex128. Weitere einfache Datentypen sind bool und string. Der String ist unter der Haube eine Menge von Bytes mit den Zeichen in UTF-8. Dies braucht den Entwickler in der Regel nicht zu kümmern, verwundert jedoch manchmal bezüglich der Indexwerte bei der zeichenweisen Iteration über den String. Letzter einfacher Typ ist der uintptr. Es ist die Referenz auf jede beliebige Speicherzelle ohne eine Typisierung und kommt nur bei der sehr systemnahen Entwicklung zum Einsatz.

func Add(is ...int) int {
s := 0
for _, i := range is {
 s += i
}
return s
}
func Div(first int, is ...int) (int, error) {
q := first
for _, i := range is {
 if i == 0 {
 return 0, errors.New("divide by zero")
 }
 q /= i
}
return q, nil
}
Listing 3: Funktionen

Zusammengesetzte Typen sind Arrays mit einer festen Größe, die mit ihnen verwandten Slices mit variabler Größe, Maps für die Abbildung von Schlüsseln auf ihnen zugeordnete Daten sowie Strukturen für die Bündelung von Feldern. Funktionssignaturen, Interfaces als Bündel von Methodensignaturen und Channels für die Übertragung von Daten zwischen Goroutines runden die Vielfalt der Datentypen ab (s. Listing 4).

type (
UUID [16]byte
UUIDs chan UUID
Words []string
Synonyms map[string]Words
MapWord func(w string) string
Mapper interface {
 Map(f MapWord, ws Words) Words
}
KeyValue struct {
 Key string
 Value []byte
}
FailedAfterError int
)
func (fae FailedAfterError) Error() string {
return fmt.Sprintf("failed after %d attempts", fae)
}
Listing 4: Zusammengesetzte Typen

Bei den Interfaces sollten zwei jedoch noch extra erwähnt werden. Das erste ist das Empty Interface ohne Methoden. Es steht für jeden beliebigen Typ, zum Beispiel als Argument oder in einem Slice. Die Methodik zur Ermittlung des tatsächlichen Typs kommt später noch zur Sprache. Er wird bewusst unschön als interface{}geschrieben, ohne einen Alias. Zweites Interface ist der oben bereits genutzte error. Dies kann jeder Typ mit der Methode Error() string sein.

Slices, Maps und Channels sind Referenztypen und werden mit make() erzeugt, zum Beispiel make(Words, 1000) oder make(Synonyms). Bei der Übergabe an eine Funktion wird hier immer eine Referenz übergeben. Bei den anderen Typen sind es Werte, die kopiert werden. Mit einem vorangestellten Asterisk sind es jedoch auch Referenzen. Eine Referenz auf ein int ist also ein *int. Diese Referenzen werden mit new() erzeugt. Praktische Kurzschreibweisen erleichtern das Leben aber ebenso wie die implizite Typenbestimmung. Also an Stellen von var s int gefolgt von einem s = Add(1, 2, 3) funktioniert auch ein s := Add(1, 2, 3) da Add() ein int zurückgibt. Dies geht auch mit Werten wie s := 0, in diesem Fall werden Standardtypen herangezogen. Wird ein anderer Typ benötigt, lässt sich dies über ein Type-Casting in der Form b := byte(0) erreichen.

Typen in Go sind keine Klassen, Go ist nicht objektorientiert. Doch für eigene Typen können Methoden definiert werden. Dies gilt nicht nur für Strukturen, sondern für jeden Typ. Dennoch übernehmen Strukturen mit Methoden häufig ähnlichen Rollen wie Klassen in objektorientierten Sprachen. Zudem können hier Typen konkret – wie auch Interfaces – eingebettet werden, sodass sich deren Methoden direkt nutzen lassen.

Listing 5 demonstriert, dass Noisemaker nicht explizit das Interface Horn deklarieren muss, dies wird durch Go implizit gehandhabt. Jeder Typ mit der Methode Honk() string ist aus Sicht der Sprache ein Horn. Und so findet zunehmend das Idiom, Interfaces erst bei der Nutzung zu definieren, seinen Weg in den Go-Code. Es wird festgelegt, was von einem Argument, von einem Typ erwartet wird.

type Horn interface {
Honk() string
}
type Noisemaker int
func (n Noisemaker) Honk() string {
return strings.Repeat("HONK", n)
}
type Car struct {
Horn
Occupants int
// ...
}
func main() {
myCar := &Car{
 Noisemaker(5),
 Occupants: 2,
 // ...
}
// Returns "HONKHONKHONKHONK".
myCar.Honk()
}
Listing 5: Noisemaker muss Horn nicht explizit deklarieren

Die Variante, die Instanz von Car zu erzeugen, setzt in Listing 6 alle Felder explizit. Zudem wird mit dem einleitenden Ampersand (&Car) eine Referenz auf die Instanz erzeugt. Letzteres ist für das Ändern von Feldern interessant, hier im Beispiel ist es nicht notwendig. Konstruktoren existieren in Go nicht. Hier kommen Funktionen mit der Instanz als Rückgabewert zum Einsatz, beispielsweise NewCar(seats int) *Car. So lassen sich auch die Felder, wie auch individuelle Methoden, privat halten, während Konstruktorfunktionen und gewünschte Methoden vom Package exportiert werden. Getter sollten hierbei im Gegensatz zu vielen Sprachen nicht das Präfix Get haben, Setter jedoch Set.

Die Kontrollkonstrukte in Go sind ebenfalls nicht spektakulär. Eine Form der Verzweigungen ist das ifbeziehungsweise if/else. Interessant ist hier, dass die auszuführenden Anweisungen, auch bei nur einer, immer in Klammern notiert werden, die Bedingung hingegen nicht. Beim Programmfluss ist es üblich, diese Verzweigungen nicht tief zu verschachteln. Vielmehr werden Bedingungen geprüft und bei Bedarf Funktionen oder Blöcke verlassen. Danach folgen weitere Bedingungen, bis dann letztendlich die eigentlichen Aktionen durchgeführt werden.

Interessant an Listing 7 sind vielleicht noch zwei kleine Go-Spezialitäten. Einerseits lassen sich Funktionsaufrufe mit defer stapeln. Beim Verlassen einer Funktion werden sie dann in umgekehrter Reihenfolge ausgeführt. Im Beispiel ist es das Schließen der Datei mit f.Close(), nachdem das Erzeugen mit der Prüfung auf einen eventuellen Fehler (if err != nil) sichergestellt wurde. Ebenso können kurze Anweisungen in einer if-Anweisung ausgeführt und nach einem Semikolon getestet werden. Das Semikolon ist wie in vielen C-artigen Sprachen der Separator zwischen zwei Ausdrücken, wird bei einem Zeilenumbruch je nach Ausdruck aber implizit gesetzt.

func WriteAll(fileName string, lines []string) error {
if len(lines) == 0 {
 return nil
}
f, err := os.Create(fileName)
if err != nil {
 return err
}
defer f.Close()
for _, line := range lines {
 if _, err = f.WriteString(line + "\n"); err != nil {
 return err
 }
}
return nil
}
Listing 7: Kontrollkonstrukte

Natürlich existiert auch eine Mehrfachverzweigung mit einem switch (s. Listing 8). Schön ist hier, dass das normale break am Ende eines Zweigs nicht benötigt wird, dafür bei Bedarf aber ein fallthrough zur Verfügung steht. Eine Variante kommt zudem ohne den switch-Ausdruck aus, dafür aber mit individuellen booleschen Ausdrücken in den case-Zweigen, die von oben nach unten ausgewertet werden. Dritte Variante ist die Prüfung eines Typs eines Interfaces mit dem Type Switch. Anschließend kann mit dem tatsächlichen Typ weitergearbeitet werden.

switch x {
default:
 fmt.Println("something else")
case 1:
 fmt.Println("one")
case 2:
 fmt.Println("two")
}
switch {
case x > 10:
 fmt.Println("big")
 fallthrough
case x > 100:
 fmt.Println("bigger")
 fallthrough
default:
 fmt.Println("extreme big")
}
switch v := x.(type) {
case int:
 processInt(v)
case string:
 processString(v)
case MyType:
 processMyType(v)
case bool, complex128:
 fmt.Println("cannot process")
}
Listing 8: switch-Anweisung

Bleibt zuletzt noch die Verzweigung via select im Rahmen der Nebenläufigkeit. Doch dazu später mehr. Nun erst mal die Schleifen, bei denen der Gopher nur das Schlüsselwort for kennt. Auch hier folgen die Bedingung und dann der Block. Die einfachste Variante ist die Endlosschleife, welche nur aus dem for und dem Block besteht (s. Listing 9). Mit einem einfachen Ausdruck entspricht die Schleife dem oft bekannten while. Auch die übliche Schleife mit Initialisierung, Bedingung und Folgeausdruck, bekannt für Zählschleifen, ist möglich. Bleibt noch die Schleife mit dem range-Ausdruck für die Iteration über Arrays, Slices, Maps und eintreffende Daten aus Channels. Abbruchbedingungen lassen sich innerhalb der Schleifen mit if prüfen, die Schleife kann mit break verlassen werden.

for {
// ...
if foo == bar {
 break
}
}
i := 0
for i < 100 {
// ...
i++
}
for i := 0; i < 100; i++ {
// ...
}
users := allUsers()
for i, user := range users {
// ...
}
Listing 9: for-Schleife

Bei der Iteration über Arrays und Slices wird als erster Rückgabewert der Index zurückgegeben, als zweites der Wert. Nimmt die Zuweisung in der Schleife nur einen Wert entgegen, ist dies nur der Index. Bei einer Iteration über eine Map sind die beiden Werte der Schlüssel und der Wert. Die Nutzung von Channels wird im Folgenden behandelt.

Ganz nebenläufig

Eine sehr wichtige Eigenschaft von Go ist die Nebenläufigkeit. Entgegen der direkten Nutzung von Threads folgt Go der Idee der Communicating Sequential Processes [Wiki] von Tony Hoare aus dem Jahr 1978. Rob Pike definiert nebenläufige Programme als Komposition unabhängig ausgeführter Prozesse. Dies erlaubt sehr natürliche und flexible Architekturen.

Die sehr leichtgewichtig implementierten Prozesse, Goroutinen genannt, werden durch die Laufzeitumgebung auf einen Thread-Pool verteilt. Die Anzahl der Goroutinen kann sehr gering sein, jedoch problemlos auch mehrere Tausend umfassen. Prinzipiell handelt es sich bei den Goroutinen nur um Funktionen beziehungsweise Methoden, die mit dem Schlüsselwort go als eigenständiger Prozess gestartet werden. Eine Process-ID, wie in Erlang/OTP, wird nicht zurückgegeben und steht somit nicht als Kontrollinstrument zur Verfügung. Hierzu sind Channels einzusetzen. Ebenso werden auch eventuelle Rückgaben der Funktionen bei Beendigung ignoriert. Dies kann ebenfalls über Channels erreicht werden.

Die Implementierung dieser Funktionen hängt vom Einsatzzweck ab. Sie können eine Aufgabe erledigen und sich beenden, auf ein Datum über einen Channel warten und ebenso ein Ergebnis vor der Beendigung zurückgeben oder dauerhaft über einen oder mehrere Channels auf Daten zur Verarbeitung warten. Hier hilft die sequenzielle Verarbeitung bei atomaren Zustandsänderungen und so bei der Vermeidung von Data Races. Auszuschließen sind sie leider dennoch nicht.

Die zur Kommunikation notwendigen Channels sind, wie bereits in Listing 4 bei den UUIDs kurz gezeigt, typisiert. Dies können auch Empty Interfaces sein, einfache Typen und Strukturen, aber auch Funktionen. Letzteres erlaubt flexible und praktische Muster. Sie werden mit erzeugt und sind normal ungepuffert. Ein Schreiber wird also blockiert, bis der Wert vom Leser entnommen wird. Mit einem zweiten Argument bei der Anlage lässt sich eine Puffergröße definieren, zum Beispiel make(chan string, 16). Das Schreiben in sowie das Lesen aus einem Channel erfolgt über den Operator
<- und der Gebrauch nach einem Channel schreibt in ihn, vor einem Channel liest aus ihm.

Das sehr einfache Beispiel in Listing 10 erzeugt einen Channel für eventuelle Fehler und schickt dann die Nutzung von WriteAll()

func main() {
fileName := "story.txt"
myStrings := GenerateSentences()
errs := make(chan error, 1)
go func() {
 err := WriteAll(fileName, myStrings)
 errs <- err
}()
 // Do other stuff ...
if err := <-errs; err != nil {
 fmt.Printf("error: %v", err)
}
}
Listing 10: Fehler-Channe

als anonyme Funktion in den Hintergrund. Nun kann main() weitere Dinge erledigen und prüft am Ende noch über den Fehlerkanal, ob das Schreiben problemlos funktioniert hat. Der Fehler-Channel wurde dabei mit einem Puffer von 1 angelegt, damit sich die Goroutine nach dem Schreiben beenden kann, unabhängig davon, was das Programm sonst noch so erledigt.

Die oben bereits angedeutete Verzweigung für die Arbeit mit Channels erfolgt über ein select mit mehreren Zweigen für jeden Channel. Zu den Channels mit Daten gesellt sich oft noch einer für ein sauberes Beenden dazu. Ein hilfreiches Package in Go ist hierfür context. Ein etwas umfangreicheres Beispiel soll dies zeigen. Es soll nach dem Aktoren-Muster arbeitet.

In Listing 11 wird in der Constructor-Funktion der vom Erzeuger gelieferte Kontext mit einem cancel() ausgestattet, welchen eine Stop()-Methode aufrufen kann, um die folgende Goroutine Collector. backend() zu beenden. Der Channel actions dient dazu, die auszuführenden Aufgaben an das Backend zu senden.

type Collector struct {
cancel context.CancelFunc
actions chan func() error
strings []string
}
func NewCollector(ctx context.Context) *Collector {
c := &Collector{
 actions: make(chan func() error, 64),
}
ctx, c.cancel = context.WithCancel(ctx)
go c.backend(ctx)
return c
}
Listing 11: Nebenläufiger String-Sammler

In dieser Form aus Listing 12 macht der Typ bisher noch nichts Fachliches. Es ist nur Infrastruktur, die sich sehr gut in einem Hilfstyp zur Wiederverwendung implementieren ließe. Nun können Aktionen definiert werden, welche sequenziell lesenden und schreibenden Zugriff auf die Felder des Typs haben (s. Listing 13).

func (c *Collector) backend(ctx context.Context) {
for {
 select {
 case <-ctx.Done():
 return
 case action := <-c.actions:
 action()
 }
}
}
func (c *Collector) do(action func() error) error {
errs := make(chan error, 1)
c.actions <- func() error {
 errs <- action()
}
return <-errs
}
func (c *Collector) doAsync(action func() error) {
c.actions <- action
}
Listing 12: Goroutine und Helfer
func (c *Collector) Append(strings ...string) {
c.doAsync(func() error {
 c.strings = append(c.strings, strings...)
 return nil
 })
}
func (c *Collector) ToUpper() {
c.doAsync(func() error {
 for i, s := range c.strings {
 c.strings[i] = strings.ToUpper(c.strings[i])
 }
 return nil
})
}
func (c *Collector) Len() int {
i := 0
c.do(func() error {
 i = len(c.strings)
})
return i
}
Listing 13: Öffentliche Programmierschnittstelle des Sammlers

Diese Methoden und weitere können nun beliebig von weiteren Goroutinen aufgerufen werden, ein Konflikt findet nicht statt. Dies wird bei komplexeren Typen natürlich spannender, an dieser Stelle hätte es auch ein Mutex getan. Zudem sollte ein generischer Typ eine ausgefeiltere interne Fehlerkontrolle mit sich bringen, einen beendeten Backend behandeln und auch den Umgang mit Timeouts anbieten. Bei aller Liebe zur Nebenläufigkeit muss natürlich dennoch die Programmierschnittstelle eines Typs sauber durchdacht werden. Zusammengehörige Felder dürfen nicht individuell gesetzt werden, sondern nur gemeinsam. Zyklische Zugriffe zwischen unterschiedlichen nebenläufigen Typen können sich blockieren, was durch asynchrone Nachrichten und Antworten sowie Timeouts entspannt werden kann. Und absolutes Lesen und Schreiben von Werten kann zwischen zeitgleich agierenden Nutzern zu Race Conditions führen. Hier helfen relative Updates, also zum Beispiel IncreaseBy()

statt Get() und Set(). Alternativ ist die Abfrage eines Handles mit Timeout für eine Aktualisierung, danach anfragende Nutzer erhalten bis zur Rückgabe des Handles eine Fehlermeldung. Hier müssen sich die Entwickler bei stark nebenläufigen Sprachen erst mal in eine neue Welt einarbeiten.

Ausblick

Googles Go ist und bleibt eine technisch orientierte Sprache, auch wenn sich problemlos geschäftsorientierte Anwendung mit flexiblen RESTful APIs entwickeln lassen. Sie gefällt durch Pragmatismus und Einfachheit im täglichen Umgang, neben der Geschwindigkeit Gründe für die zunehmende Verbreitung. Dazu kommen noch viele weitere kleine, angenehme Aspekte. Doch es gibt natürlich auch Kritikpunkte von Einsteigern. Im Wesentlichen sind dies der Weg der Fehlerbehandlung und der Verzicht auf Generics. Beide werden allerdings schon für Go 2 [Go2] diskutiert.
Im zweiten Teil werden aktuelle, in Go implementierte Projekte betrachtet.

Links

[Go]
The Go Programming Language,
https://golang.org/

[Go2]
R. Griesemer, Go 2, here we come!, The Go Blog, 29.11.2018,
https://blog.golang.org/go2-here-we-come

[Wiki]
https://de.wikipedia.org/wiki/Communicating_ Sequential_Processes

. . .

Author Image

Frank Müller

Author
Zu Inhalten
Frank Müller ist seit über dreißig Jahren in der IT zu Hause und im Netz vielfach als @themue anzutreffen. Das Interesse an Googles Sprache Go begann 2009 und führte inzwischen zu einem Buch sowie mehreren Artikeln und Vorträgen zum Thema.

Artikel teilen