Golang-Tutorial

So programmieren Sie mit Go

09.03.2023
Von 
Martin Heller schreibt als freier Autor für die Schwesterpublikation InfoWorld.
Go ist eine prägnante, einfache und sichere Programmiersprache. Mit diesem Tutorial können Sie sich selbst davon überzeugen.
Zeit, Go eine Chance zu geben? Unser Tutorial unterstützt Sie beim Einstieg.
Zeit, Go eine Chance zu geben? Unser Tutorial unterstützt Sie beim Einstieg.
Foto: samjoule - shutterstock.com

Go ist eine Open-Source-Programmiersprache von Google, die es einfach macht, simple, zuverlässige und effiziente Software zu programmieren. Go ist Teil der Programmiersprachen-Linie, die mit Tony Hoares "Communicating Sequential Processes" ihren Anfang nahm und zusätzlich folgende Programmiersprachen umfasst:

Das Go-Projekt zählt derzeit mehr als 1.800 Kontributoren und wird von Rob Pike geleitet, seines Zeichens Distinguished Engineer bei Google. Pike entwickelte Go ursprünglich als C++-Alternative, weil er die Nase voll hatte von C++-Kompilierungen. Auf die Frage, was ihn nach der Einführung von Go im Jahr 2012 am meisten überrascht habe, antwortete Pike: "Wir hatten erwartet, dass C++-Programmierer Go als Alternative sehen würden. Stattdessen kommen die meisten Go-Programmierer von Sprachen wie Python und Ruby."

Eine damals nahezu einhellige Forderung aus der C++- und Java-Community: Go um Klassen und Generics zu ergänzen. Dagegen wehrten sich Pike und andere lange - bis zum Jahr 2022. Mit Go in Version 1.18 hielten Generics Einzug.

In diesem Artikel werfen wir einen Blick darauf, wie Go sich von anderen Programmiersprachen abhebt. Natürlich betrachten wir dabei auch Concurrency Patterns und die neuen Generics.

Slices

Go erweitert die Idee der Arrays um Slices, die eine variable Größe aufweisen. Ein Slice verweist auf ein Array von Werten und enthält eine Länge. Ein Beispiel: [ ]T ist ein Slice mit Elementen vom Typ T. Im folgenden Code verwenden wir Slices von Slices von Bytes ohne Vorzeichen, um die Pixel eines von uns erzeugten Bildes zu speichern. Dabei reichen die Pixelwerte von 0 bis 255. Go-Programme werden mit package main gestartet. Das import-Statement stellt eine erweiterte Version der include-Anweisung von C und C++ dar.

package main

import "code.google.com/p/go-tour/pic"

func Pic(dx, dy int) [][]uint8 {

slice := make([][]uint8, dy)

for i := range slice {

slice[i] = make([]uint8, dx)

for j := range slice[i] {

slice[i][j] = uint8(i * j)

}

}

return slice

}

func main() {

pic.Show(Pic)

}

Die :=-Syntax deklariert und initialisiert eine Variable, der Compiler schließt auf einen Typ, wann immer er kann. Beachten Sie dabei auch, dass make verwendet wird, um Slices und einige andere Typen zu erstellen. Eine for...range-Schleife ist das Äquivalent zur for...in-Schleife von C#. Das in Abbildung 1 gezeigte Muster wird durch den Ausdruck in der inneren Schleife oben bestimmt: (i*j). Weitere Informationen finden Sie im pic-Package und seinem Quellcode.

Abbildung 1: Ein Muster, das Slices in Go demonstriert.
Abbildung 1: Ein Muster, das Slices in Go demonstriert.

Maps

Das map-Statement in Go ordnet Keys zu Values zu. Wie bei slice erstellen Sie eine Map mit make, nicht mit new. Im folgenden Beispiel ordnen wir String-Keys Integer-Werten zu. Dieser Code demonstriert das Einfügen, Aktualisieren, Löschen und Testing von Map-Elementen.

package main

import "fmt"

func main() {

m := make(map[string]int)

m["Answer"] = 42

fmt.Println("The value:", m["Answer"])

m["Answer"] = 48

fmt.Println("The value:", m["Answer"])

delete(m, "Answer")

fmt.Println("The value:", m["Answer"])

v, ok := m["Answer"]

fmt.Println("The value:", v, "Present?", ok)

}

Das ist der Print-Output des Programms:

The value: 42

The value: 48

The value: 0

The value: 0 Present? false

Structs und Methods

In Go gibt es keine Klassen, sondern eine sogenannte struct. Dabei handelt es sich um eine Sequenz benannter Elemente, die fields genannt werden. Jedes field hat einen name und einen type. Eine method ist eine Funktion mit einem Empfänger. Eine method-Declaration bindet einen Identifier (den method name) an eine Methode und assoziiert diese mit dem Base Type des Empfängers.

In diesem Beispiel deklarieren wir eine Vertex struct, die zwei Fließkommafelder (fields) - X und Y - und eine Methode - Abs - enthält. Felder, die mit Großbuchstaben beginnen, sind public. Felder, die mit Kleinbuchstaben beginnen, sind private. Felder und Methoden können durch die Punktnotation (.) adressiert werden, während ampersands (&) wie in C für Pointer stehen. Dieses Programm erzeugt den Print Output 5.

package main

import (

"fmt"

"math"

)

type Vertex struct {

X, Y float64

}

func (v *Vertex) Abs() float64 {

return math.Sqrt(v.X*v.X + v.Y*v.Y)

}

func main() {

v := &Vertex{3, 4}

fmt.Println(v.Abs())

}

Interfaces

Ein Interface Type wird durch ein Methoden-Set definiert. Ein Value kann dabei jeden Wert enthalten, der diese Methoden implementiert. Im folgenden Beispiel definieren wir ein Interface, Abser, und eine Variable (a) vom Typ Abser:

package main

type Abser interface {

Abs() float64

}

func main() {

var a Abser

f: MyFloat(-math.Sqrt2

v = Vertex{3, 4}

a = f // a MyFloat implements Abser

a = &v // a *Vertex implements Abser

// In the following line, v is a Vertex (not *Vertex)

// and does NOT implement Abser.

a = v

fmt.Println(a.Abs())

}

type MyFloat float64

func (f MyFloat) Abs() float64 {

if f < 0 {

return float64(-f)

}

return float64(f)

}

type Vertex struct {

X, Y float64

}

Beachten Sie, dass die Zuweisungen a=f und a=&v funktionieren, die Zuweisung a=v aber nicht einmal kompiliert werden kann. Die Abs-Methode von Vertex, die Sie im vorigen Abschnitt gesehen haben, hat als Empfänger einen Pointer, der auf den Vertex-Type verweist. *Vertex implementiert also Abser, Vertex jedoch nicht.

switch

Das switch-Statement in Go ähnelt den Pendants anderer, C-ähnlicher Programmiersprachen - mit dem Unterschied, dass case nicht nur einfache Werte, sondern auch Types oder Expressions sein kann. Cases werden automatisch abgebrochen, es sei denn, sie enden mit fallthrough-Statements. Die Cases werden in der Reihenfolge evaluiert, in der sie definiert sind.

package main

import (

"fmt"

"runtime"

)

func main() {

fmt.Print("Go runs on ")

switch os = runtime.GOOS; os {

case "darwin":

fmt.Println("macOS.")

case "linux":

fmt.Println("Linux.")

default:

// freebsd, openbsd,

// plan9, windows...

fmt.Printf("%s.", os)

}

}

Goroutines

Goroutines sind im Grunde extrem leichtgewichtige Threads, ganz im Sinne von Tony Hoares "Communicating Sequential Processes". In dem folgenden Beispiel ruft die erste Zeile von func main die say-Funktion asynchron auf, während die zweite Zeile sie synchron aufruft. Der Unterschied liegt in der Verwendung des go-Qualifiers für die asynchrone Goroutine:

package main

import (

"fmt"

"time"

)

func say(s string) {

for i := 0; i < 5; i++ {

time.Sleep(100 * time.Millisecond)

fmt.Println(s)

}

}

func main() {

go say("world")

say("hello")

}

Goroutines, Channels und select-Statements bilden den Kern der hochskalierbaren Concurrency von Go, einem der stärksten "Verkaufsargumente" der Sprache. Darüber hinaus verfügt Go auch über herkömmliche Synchronisationsobjekte, die jedoch nur selten benötigt werden. Dieses Programm gibt aus:

hello

world

hello

world

hello

world

hello

world

hello

Channels

Channels bieten in Go einen Mechanismus, mit dem Funktionen, die parallel ausgeführt werden, kommunizieren können. Dazu senden und empfangen sie Values eines bestimmten Elementtyps. Hier ein Beispiel:

package main

import "fmt"

func sum(s []int, c chan int) {

sum := 0

for _, v := range s {

sum += v

}

c <- sum // send sum to c

}

func main() {

s := []int{7, 2, 8, -9, 4, 0}

c := make(chan int)

go sum(s[:len(s)/2], c)

go sum(s[len(s)/2:], c)

x, y := <-c, <-c // receive from c

fmt.Println(x, y, x+y)

}

Beachten Sie, dass der Wert eines nicht initialisierten Kanals gleich Null ist. c = make(chan int) erzeugt einen bidirektionalen Kanal für ganze Zahlen. Wir könnten auch unidirektionale Sende- (<-c) und Empfangskanäle (c<-) erzeugen. Anschließend rufen wir sum asynchron mit Slices der ersten und zweiten Hälfte von a auf. Dann empfangen die Integer-Variablen x und y die beiden Summen aus dem Kanal. Im Ausdruck "for _, v range a" bewirkt der Unterstrich (_), dass der erste Ergebniswert aus dem for...range-Loop - der Index - ignoriert wird. Der Programm-Output lautet 17 -5 12.

Range und Close

Im folgenden Beispiel ist zu sehen, wie ein Sender einen Kanal schließen kann (close), um anzuzeigen, dass keine weiteren Werte gesendet werden. Empfänger können prüfen, ob ein Kanal geschlossen wurde, indem sie der empfangenen Expression einen zweiten Parameter zuweisen.

package main

import (

"fmt"

)

func fibonacci(n int, c chan int) {

x, y := 0, 1

for i := 0; i < n; i++ {

c <- x

x, y = y, x+y

}

close(c)

}

func main() {

c := make(chan int, 10)

go fibonacci(cap(c), c)

for i := range c {

fmt.Println(i)

}

}

Der for-Loop in der dritten Zeile von main (for i := range c) empfängt wiederholt Werte aus dem Kanal, bis dieser geschlossen wird. Die cap des Kanals ist die Kapazität, also die Größe des Puffers im Kanal. Sie wird als optionales zweites Argument gesetzt, wenn Sie einen Kanal erstellen, wie in der ersten Zeile von main. Beachten Sie die kompakte Form der Zuweisungsanweisungen in der fibonacci-Funktion. Den Programm-Output bilden die ersten zehn Werte der Fibonacci-Reihe, 0 bis 34.

select

Ein select-Statement wählt aus einer Reihe möglicher send- und receive-Operationen diejenigen aus, die ausgeführt werden sollen. Dabei bestehen Ähnlichkeiten zum switch-Statement, allerdings beziehen sich alle Cases auf Kommunikationsvorgänge. select blockiert, bis einer ihrer Cases ausgeführt werden kann und führt diesen dann aus. Wenn mehrere Fälle bereit sind, kommt das Zufallsprinzip zur Anwendung.

package main

import "fmt"

func fibonacci(c, quit chan int) {

x, y := 0, 1

for {

select {

case c <- x:

x, y = y, x+y

case <-quit:

fmt.Println("quit")

return

}

}

}

func main() {

c := make(chan int)

quit := make(chan int)

go func() {

for i := 0; i < 10; i++ {

fmt.Println(<-c)

}

quit <- 0

}()

fibonacci(c, quit)

}

Hier ruft die main-Funktion die fibonacci-Funktion mit zwei ungepufferten Kanälen auf - einen für Ergebnisse und einen für ein quit-Signal. Die fibonacci-Funktion verwendet ein select-Statement, um auf beide Kanäle zu warten. Die anonyme, asynchrone go-Funktion, die in der dritten Zeile von main beginnt, wartet darauf Werte (<-c) zu empfangen und gibt diese dann aus. Nach zehn Werten setzt sie den quit-Kanal, damit die fibonacci-Funktion stoppt.

Concurrency Patterns

Wir haben uns einige Alleinstellungsmerkmale der Programmierprache Go betrachtet. Nun schauen wir uns an, wie diese in Programmierbeispielen zusammenwirken. Dabei beginnen wir mit einigen Concurrency Patterns in Go - beide entstammen einem Vortrag von Rob Pike aus dem Jahr 2012:

Concurrency Pattern #1: fan in

In diesem Beispiel verwenden wir select, um eine Fan-In-Goroutine zu erstellen, die zwei String-Eingangskanäle - input1 und input2 - zu einem ungepufferten Ausgangskanal - c - kombiniert. Die select-Anweisung ermöglicht fanIn, beide Eingangskanäle gleichzeitig abzuhören und denjenigen, der bereit ist, an den Output-Channel weiterzuleiten. Dabei spielt es keine Rolle, dass in beiden Fällen derselbe temporäre Variablenname verwendet wird, um die Zeichenfolge aus den jeweiligen Eingangskanälen zu speichern.

package main

func fanIn(input1, input2 <-chan string) <-chan string {

c := make(chan string)

go func() {

for {

select {

case s := <-input1: <- s

case s := <-input2: c <- s

}

}

}()

return c

}

Concurrency Pattern #2: Parallele Suche

Folgendes Concurrency-Beispiel implementiert eine parallele Suche im Internet, ähnlich wie die Google-Suchmaschine. replicas ...Search ist ein variabler Parameter für die Funktion, während sowohl Search als auch Result Typen sind, die an anderer Stelle definiert sind:

package main

func First(query string, replicas ...Search) Result {

c := make(chan Result)

searchReplica := func(i int) { c <- replicas[i](query) }

for i := range replicas {

go searchReplica(i)

}

return <-c

}

Der Caller übergibt eine bestimmte Anzahl von (N) Suchserverfunktionen an die First-Funktion. Diese erstellt einen Kanal c für Ergebnisse, definiert eine Funktion zur Abfrage des i-ten Servers und speichert sie in searchReplica. First ruft dann searchReplica asynchron für alle N Server auf und gibt die Antwort immer auf Kanal c zurück.

Packages in Go

Werfen wir nun einen Blick auf einige Packages.

Das http-Package

Das Go-Package net/http bietet HTTP-Client- und -Server-Implementierungen. Dieses Beispiel implementiert einen einfachen Webserver, der den Inhalt des Verzeichnisses /usr/share/doc an einen Webclient zurückgibt:

package main

import (

"log"

"net/http"

)

func main() {

// Simple static webserver:

log.Fatal(http.ListenAndServe(":8080", http.FileServer(http.Dir("/usr/share/doc"))))

}

Dieses Beispiel funktioniert in der Online-Umgebung von Go Playground nicht richtig. Wenn Sie es jedoch auf einer Mac-Befehlszeile ausführen, gibt es folgendes an einen Webbrowser zurück, der nach http://localhost:8080/ fragt:

  • bash/

  • ccid/

  • cups/

  • groff/

  • ntp/

  • postfix/

Das Template-Package

Das html/template-Package implementiert data-driven Templates, um HTML Output zu generieren, der gegen Code-Injektion abgesichert ist. Zum Beispiel:

package main

import "html/template"

func main() {

t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)

err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")

}

Der obige Code erzeugt den HTML-Output:

Hello, &lt;script&gt; alert(&#39; you have been pwned&#39;)&lt;/script&gt;!

Ohne die durch das html/template-Package in diesem Beispiel hinzugefügte Maskierung hätten wir folgende lauffähige JavaScript-Zeichenkette erzeugen können:

Hello, <script> alert(' you have been pwned')</script>!

Generics in Go

Wie bereits erwähnt waren Generics ein lange nachgefragtes Feature für Go. Nun sind sie tief in die Coding-Sprache integriert und beinhalten die Idee der Typbeschränkungen. Das bedeutet, dass die Typparameter einer Funktion zwischen eckigen Klammern und vor den Argumenten der Funktion stehen. Im Allgemeinen ist eine Funktion, die mit generischen Typen implementiert wird, effizienter als eine, die mit dem any-Type implementiert wird. Wenn Sie in Ihrem Code eine Gruppe von Funktionen sehen, die sich nur hinsichtlich ihrer Argumenttypen unterscheiden, sollten Sie sich überlegen, stattdessen eine generische Version zu schreiben. Die folgenden zwei Beispiele entstammen "A Tour of Go".

Generics-Beispiel #1: Indexfunktion für vergleichbare Typen

Im folgenden Code weist func Index[T comparable](s []T, x T) einen Typparameter von T auf und besagt, dass s ein Slice eines beliebigen Types T ist, der die eingebaute Einschränkung comparable erfüllt. Beachten Sie, dass x ebenfalls ein Wert desselben Typs ist.

package main

import "fmt"

// Index returns the index of x in s, or -1 if not found.

func Index[T comparable](s []T, x T) int {

for i, v := range s {

// v and x are type T, which has the comparable

// constraint, so we can use == here.

if v == x {

return i

}

}

return -1

}

func main() {

// Index works on a slice of ints

si := []int{10, 20, 15, -10}

fmt.Println(Index(si, 15))

// Index also works on a slice of strings

ss := []string{"foo", "bar", "baz"}

fmt.Println(Index(ss, "hello"))

}

Da sowohl Ganzzahlen als auch Zeichenketten vergleichbar sind, akzeptiert die Index-Funktion beide. Dieses Beispiel gibt 2 und dann -1 aus, was korrekte Antworten sind. Beachten Sie, dass Go, wie C und C++, bei 0 mit der Zählung beginnt.

Generics-Beispiel #2: Generische Typen

Dieses Beispiel demonstriert eine einfache Typendeklaration für eine Linked List, die einen beliebigen Werttyp enthält. Als Übung sind Sie eingeladen, die folgende Listenimplementierung um Funktionalität zu erweitern:

package main

// List represents a singly-linked list that holds

// values of any type.

type List[T any] struct {

next *List[T]

val T

}

func main() {

}

Wie viel oder wenig Funktionalität Sie hinzufügen, bleibt Ihnen überlassen. Ich schlage vor, zumindest Next() für Elemente und New(), Front(), Back() sowie InsertAfter() für Listen zu implementieren. Eine Add()-Methode, die im Wesentlichen Back() und InsertAfter() ausführt, wäre ebenfalls gut - genauso wie eine Length()-, eine Index()-, eine Remove()- und vielleicht auch eine InsertBefore()-Methode.

Programmieren mit Go: Weitere Ressourcen

Unter folgenden Links erfahren Sie mehr über Go:

(fm)

Dieser Beitrag basiert auf einem Artikel unserer US-Schwesterpublikation InfoWorld.