Inhaltsverzeichnis
Go lernen
go1.22.2 / Ubuntu 24.04.3 LTS
Einleitung
- Installation
> apt install golang-go > go version go version go1.22.2 linux/amd64
- Hallo Welt
> vim hallo_gopher.go package main import ( "fmt" ) func main() { fmt.Println("Hallo Gopher!") } > go build hallo_gopher.go > ./hallo_gopher Hallo Gopher!
Das Erste Go-Projekt
- Erstes Go-Projekt
> vim main.go package main import ( "fmt" ) func main() { fmt.Println("Hallo Go-Projekt!") } > go build main.go > ./main Hallo Go-Projekt!
- Hilfe
> go help build | less
- Kode formatieren
> go fmt main.go
- Dokumentation
> go doc fmt.Println package fmt // import "fmt" func Println(a ...any) (n int, err error) Println formats using the default formats for its operands and writes to standard output. Spaces are always added between operands and a newline is appended. It returns the number of bytes written and any write error
Die Datentypen in Go
- Einfache Datentypen
- String, für Zeichenketten
stringin UTF-8-Kodierung
- Boolean, für boolesche Wahrheitswerte
bool: true oder false
- Zahlen, für von Integer bis Gleitkomma
int32int64float32float64
- komplexe Zahlen…
- Zusammengesetzte Datentypen
Array, Sequenz mehrerer Elemente mit fixer LängeSlice, Sequenz mehrerer Elemente mit variabler LängeMap, Wörterbuch, das Schlüssel auf einen Wert abbildet
Variablen und Konstanten
- Variablen
... func main() { // Deklaration und Zuweisung in langer Form var s1 string = "eine Zeichenkette" var i1 int = 123 var b1 bool = true fmt.Println(s1, i1, b1) }
- Package-Variable
... // Diese Variable ist im ganzen Package verwendbar var aPackageVar string = "eine globale Variable" func main() { fmt.Println("Hallo Go-Projekt!") }
- Konstanten
package main import ( "fmt" ) // Diese Konstante ist im ganzen Package verwendbar const aPackageKons = "eine globale Konstante" func main() { const k1 = "eine Zeichenkette" const k2 = 123 fmt.Println(k1, k2, k2+k2) fmt.Println(aPackageKons) }
Arrays und Slices
Arrays und Slices sind Sequenzen mehrerer Elemente.
Arrays haben eine feste Länge.
Slices haben eine variable Länge, es entspricht einer "Liste" in C++ oder Java.
- Array
package main import ( "fmt" ) func main() { // Array (mit 2 Elementen) initialisieren und füllen array01 := [2]string{"Element1", "Element2"} fmt.Println("unverändert", array01) // ein Element im Array ändern, das erste Element hat den Index "0" array01[1] = "Fritz" fmt.Println("verändert", array01) }
- Slice
package main import ( "fmt" ) func main() { // Slice mit make initialisieren slice01 := make ([]string, 2) slice01[0] = "Anita" slice01[1] = "Rom" fmt.Println(slice01) // Slice mit Werten initialisieren slice02 := []string{"Otto", "Hamburg"} fmt.Println(slice02) // Slice erweitern slice02 = append(slice02, "Bär", "Berlin") fmt.Println(slice02) }
Maps für Schlüssel/Werte-Paare
- Map
package main import ( "fmt" ) func main() { // Map mit make initialisieren var map01 = make (map[string]int) map01["Anita"] = 23 map01["Berta"] = 32 fmt.Println(map01) // Map mit Werten initialisieren var map02 = map[string]int{"Papa": 54, "Mama": 45} fmt.Println(map02) // Wert zu einem Schlüssel abrufen fmt.Println("Alter von Papa:", map02["Papa"]) }
Structs mit Feldern nutzen und definieren
In Go können mehrere Werte zu Strukturen ("Structs") zusammengefasst werden.
Mit Structs können Sie Daten kapseln, und das ganz ohne Objektorientierung.
Ein Struct ist ein einfacher Datenkontainer mit Feldern.
- Struct
package main import ( "fmt" ) type Struktur01 struct { Name string // mit großem Anfangsbuchstaben ist es ein öffentliches Feld alter int // mit kleinem Anfangsbuchstaben ist es ein privates Feld } // Struct einbinden type Struktur02 struct { Struktur01 // das Struct "Struktur02" hat nun alle Felder, die das Struct "Struktur01" hat und es können zusätzliche hinzugefügt werden passwort string } func main() { // normales Struct c := Struktur01{Name: "Alfred Nobel", alter: 63} fmt.Println(c) // ein Struct in ein anderes Struct einbinden d := Struktur02{passwort: "geheim"} d.Name = "Drachentöter" fmt.Println(d) // leeres Struct; es werden die initialen Werte ("0" und "leer") der Felder in Struktur01 ausgegeben var leereStruktur Struktur01 fmt.Println(leereStruktur) }
Pointer
- pointer.go
package main import ( "fmt" ) type Struktur01 struct { Name string // mit großem Anfangsbuchstaben ist es ein öffentliches Feld alter int // mit kleinem Anfangsbuchstaben ist es ein privates Feld } func main() { // normales Struct c := Struktur01{Name: "Alfred Nobel", alter: 63} // Pointer p auf Struct c var p *Struktur01 = &c // Pointer mit * auflösen und Wert zuweisen (Überschreiben) (*p).Name = "Gorilla" fmt.Println("c:", c) fmt.Println("Pointer auf c:", p) }
Der leere Wert nil
- nil.go
package main import ( "fmt" ) type Struktur01 struct { Name string // mit großem Anfangsbuchstaben ist es ein öffentliches Feld alter int // mit kleinem Anfangsbuchstaben ist es ein privates Feld } func main() { // Pointer ist nil var p *Struktur01 // prüfen ob der Pointer p nil ist fmt.Println("Pointer p ist nil:", p == nil) // Slice ist nil var s []float64 fmt.Println("Slice s ist nil:", s == nil) // Map ist nil var m map[string]float64 fmt.Println("Map m ist nil:", m == nil) }
Fehler
In Go gibt es keine Spezialbehandlung für Fehler. In Go sind Fehler Werte wie Strings, Integer oder Struts.
- Fehler.go
package main import ( "fmt" ) func main() { // Error err := fmt.Errorf("Benutzer %v wurde nicht gefunden", "Spenzer") fmt.Println("Error err:", err) }
In Go gibt es für Fehler den Typ "error". "error" ist ein gewöhnlicher Go-Datentyp, jedoch mit besonderer Bedeutung.
Den Kontrollfluss strukturieren
Kontrollstruktur if/else
- kontrolle_if.go
package main import ( "fmt" ) func main() { // if zweifler := "gehen" if zweifler != "laufen" { fmt.Println("Du sollst jetzt nicht laufen.") } // if/else geschwindigkeit := 100 if geschwindigkeit < 120 { fmt.Println("Du bist zu langsam.") } else if geschwindigkeit == 120 { fmt.Println("Die Geschwindigkeit stimmt.") } else { fmt.Println("Du bist zu schnell.") } }
Kontrollstruktur Switch
- kontrolle_switch.go
package main import ( "fmt" "time" ) func main() { // Switch mit Ausdruck geschwindigkeit := 100 switch { case geschwindigkeit < 120: fmt.Println("Du bist zu langsam.") case geschwindigkeit == 120: fmt.Println("Die Geschwindigkeit stimmt.") default: fmt.Println("Du bist zu schnell.") } // Switch über Typ switch time.Now().Weekday() { case time.Saturday: fmt.Println("Es ist Sonnabend.") case time.Sunday: fmt.Println("Es ist Sonntag.") default: fmt.Println("Es ist ein Arbeitstag.") } }
for-Schleife
- kontrolle_for.go
package main import ( "fmt" ) func main() { // klassische for-Schleife for i := 1; i < 7; i++ { fmt.Println("Durchlauf (for)", i) } // while-Schleife j := 1 for j < 7 { fmt.Println("Durchlauf (while)", j) j++ } // Endlos-Schleife //k := 1 //for { // fmt.Println("Durchlauf (while)", k) // k++ //} // for-each-Schleife mit range slice := []string{"Otto", "Didi", "Heinz"} for index, character := range slice { fmt.Println(index, "Character", character) } }
Übung
- kontrolle_uebung.go
package main import ( "fmt" ) func main() { // Übung: Alter vom Hund in Menschenjahren und Hundejahren for dogAge := 1; dogAge < 11; dogAge++ { HumanAge := dogAge * 7 fmt.Printf("Dog: %d, Human %d\n", dogAge, HumanAge) } }
Funktionen und Methoden
einfache Funktionen
- einfache_Funktionen.go
package main import ( "fmt" ) func humanAge1(dogAge int) int { return dogAge * 7 } func humanAge2(dogAge int) (humanAge int) { humanAge = dogAge * 7 return } func main() { // Übung: Alter vom Hund in Menschenjahren und Hundejahren hassoAge := 3 fmt.Println("1. Menschenalter von Hasso", humanAge1(hassoAge)) fmt.Println("2. Menschenalter von Hasso", humanAge2(hassoAge)) }
Funktionen mit mehreren Parametern und Rückgaben
- Funktionen_mit_mehreren_Parametern.go
package main import ( "fmt" ) func humanAge(dogAge int) int { return dogAge * 7 } func humanAges(dog1, dog2 int) (int, int) { return humanAge(dog1), humanAge(dog2) } func main() { human1, human2 := humanAges(3, 5) fmt.Println("Human Ages:", human1, ",", human2) }
Methoden an Strukturen definieren
- Struct_mit_Methode.go
package main import ( "fmt" ) // Struct type Struktur01 struct { Name string // mit großem Anfangsbuchstaben ist es ein öffentliches Feld alter int // mit kleinem Anfangsbuchstaben ist es ein privates Feld } // Methode func (c Struktur01) methode01() { fmt.Println("Ausgabe der Methode:", c.Name) } func main() { // normales Struct c := Struktur01{Name: "Alfred Nobel", alter: 63} // Aufruf Methode an Struct c.methode01() fmt.Println(c) }
Mit Fehlern in Funktionen umgehen
- Fehler_in_Funktionen.go
package main import ( "fmt" ) // Struktur type Congressman struct { Name string AccountBalance float64 } // Fehler als Rückgabewert // Der Kontostand eines bestechlichen Abgeordneten wird von dieser Methode erhöht // ist ein Abgeordneter nicht bestechlich, wird ein Fehler ausgegeben func (c Congressman) bestechung(amount float64) error { // ist der Name des Abgeordneten nicht Peter, so ist er nicht bestechlich if c.Name != "Peter" { return fmt.Errorf("%v ist nicht koruppt", c.Name) } c.AccountBalance += amount fmt.Println(c.Name, "hat", c.AccountBalance) return nil } func main() { //c := Congressman{Name: "Jacjie", AccountBalance: 8000.0} c := Congressman{Name: "Peter", AccountBalance: 8000.0} // Fehler behandeln err := c.bestechung(5000.0) if err != nil { fmt.Println("Ein Abgeordneter konnte nicht bestochen werden:", err) } fmt.Println(c) }
generische Funktionen
- generische_Funktionen.go
package main import ( "log" ) // generische Funktion func humanAge[T int | float64](dogAge T) T { return dogAge * 7 } func main() { // Funktion mit Typ Integer aufrufen var i int = humanAge(3) log.Println(i) // Funktion mit Typ Fließkomma aufrufen var j float64 = humanAge[float64](4.9) log.Println(j) }
anonyme Funktionen
- anonyme_Funktionen.go
package main import ( "fmt" "net/http" ) func main() { // anonyme Funktion definieren und ausführen func () { fmt.Println("Hallo Welt") }() // anonyme HTTP-Handler-Funktion http.HandleFunc("/", func (w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hallo Welt")) }) http.ListenAndServe(":8080") }
verzögerte Ausführung
- verzögerte_Ausführung.go
package main import ( "fmt" ) // mit defer kann eine Ausführung verzögert werden func halloDefer() { defer fmt.Print("Welt\n") // defer wird auch bei einem Fehler ausgeführt // hier greifen wir in einem Slice auf eine Stelle (Index) zu, // die nicht existiert -> Fehler //var s = []string{} //fmt.Println("Huch ", s[42]) fmt.Print("Hallo ") } func main() { // verzögerte Ausführung mit defer halloDefer() }
Typische Anwendungsfälle für defer sind "Dateien schließen", "Dateien löschen" oder "Speicher freigeben".
Schnittstellen mit Interfaces
Das Interface
- hallo_interface.go
package main import ( "fmt" ) type Congressman struct { Name string } func (c Congressman) greet() { fmt.Println("Hallo", c.Name) } type Enemy struct{} func (e Enemy) greet() { fmt.Println("Go to hell!") } // Interface definieren type Greeter interface { greet() } // Interface nutzen func passBy(g1, g2 Greeter) { g1.greet() g2.greet() } func main() { // Interface c := Congressman{Name: "Frank"} e := Enemy{} passBy(c, e) }
Interfaces werden in Go nicht explizit, sondern implizit integriert.
Das Interface Stringer aus der Std-Lib
- Stringer.go
package main import ( "fmt" ) type Struktur01 struct { Name string } func (c Struktur01) Sring() string { return "Hallo! " + c.Name } func main() { c := Struktur01{Name: "Alfred Nobel"} fmt.Println(c) }
$ go doc fmt.Stringer
package fmt // import "fmt"
type Stringer interface {
String() string
}
Stringer is implemented by any value that has a String method, which defines
the “native” format for that value. The String method is used to print
values passed as an operand to any format that accepts a string or to an
unformatted printer such as Print.
Das leere Interface
- leeres_interface.go
package main import ( "fmt" ) func main() { // Das leere Interface var i interface{} = "Hallo" // Type Assertion heißt in anderen Sprachen Cast -> Typ-Umwandlung // Type Assertion ohne Prüfung var s string = i.(string) fmt.Println(s) // Type Assertion mit Prüfung s2, ok := i.(string) fmt.Println(s2, "ist es ein String?", ok) // falsche Type Assertion ohne Prüfung erzeugt Panic var f float64 = i.(float64) fmt.Println(f) }
Das io.Reader-Interface JSON verarbeiten
- io-Reader-Interface.go
package main import ( "fmt" "strings" "encoding/json" ) func main() { // normales Struct const jsonBody = `{ "Nachricht": "Hallo Leser!" }` var jsonMap map[string]string r := strings.NewReader(jsonBody) json.NewDecoder(r).Decode(&jsonMap) fmt.Println(jsonMap) }
Nebenläufig programmieren
Mit Go-Routinen und Channels nebenläufig programmieren
- Go-Routine.go
package main import ( "fmt" ) func Hallo(gruss string) { fmt.Println("Hallo", gruss) } func main() { // Go-Routine go Hallo("Welt") }
- Go-Channel.go
package main import ( "fmt" ) // Deklarieren und Initialisieren //a := make(chan string) // // Nachricht über Channel senden //a <- "Hallo" // // Nachricht von Channel empfangen, // "Pfeil" gibt Richtung des Datenflusses an //value = <-a // //func Hallo(gruss string) { // fmt.Println("Hallo", gruss) //} // //func main() { // // Go-Channel // go Hallo("Welt") //} func main() { // 1. Channel erzeugen money := make(chan int) go func () { // 3. Receive auf Channel money amount := <-money fmt.Println("empfangen:", amount, "€") }() // 2. an Channel senden money <- 200 }
- 2-Sekunden_Go-Channel.go
package main import ( "fmt" "time" ) func cashflow(money chan int) { amount := <-money fmt.Println("empfangen:", amount, "€") } func main() { money := make(chan int) go cashflow(money) money <- 1000 time.Sleep(2 * time.Second) }
- mehrere_Go-Channels.go
package main import ( "fmt" "time" ) func cashflow(money chan int) { // mit Select kann man gleichzeitig auf mehrere Channels lauschen select { case amount := <-money: fmt.Println("empfangen:", amount, "€") case <-time.After(1 * time.Second): fmt.Println("...kein Geld bekommen!!!") } } func main() { money := make(chan int) go cashflow(money) // money <- 1000 time.Sleep(2 * time.Second) }
- parallele_Worker.go
package main import ( "fmt" "time" "sync" ) func worker(id int) { fmt.Printf("Worker %d startet\n", id) // durch schlafen wird aufwändige Verarbeitung simuliert time.Sleep(time.Second) fmt.Printf("Worker %d ist fertig\n", id) } func main() { // Parallele Worker mit der WaitGroup var wg sync.WaitGroup // hier werden 5 Worker in einer Schleife nacheinander gestartet for i := 1; i <= 5; i++ { wg.Add(1) // weil sich alle Worker die Variable "i" teilen, brauchen wir hier noch eine eigene Kopie von "i" i := i go func() { defer wg.Done() worker(i) }() } // hier warten wir, bis alle Worker fertig sind wg.Wait() }
- parallele_Worker_-_Debatte.go
package main import ( "fmt" "time" "math/rand" ) func speaker(name string, debate chan int) { for { microphone := <-debate // auf Mic warten (Nachricht empfangen) fmt.Printf("Turn %v: %v says '%v'\n", microphone, name, randomAnswer()) time.Sleep(400 * time.Millisecond) microphone++ debate <- microphone // Mic zurückgeben (Nachricht senden) } } func randomAnswer() string { answers := []string{"Ich habe Recht", "Nimm dieses Argument.", "Aber ich habe diese Erfahrung gemacht.", "Du ***"} return answers[rand.Intn(len(answers)-1)] } func main() { debate := make(chan int) go speaker("Jackie", debate) go speaker("Frank", debate) microphone := 1 debate <- microphone time.Sleep(2 * time.Second) <-debate }
Praktische Aufgabe: Webserver für Go Proverbs
Basisprogramm: Webserver Proverbs
> go mod init proserve > go get github.com/jboursiquot/go-proverbs go: downloading github.com/jboursiquot/go-proverbs v0.0.2 go: added github.com/jboursiquot/go-proverbs v0.0.2
- Webserver_Proverbs.go
package main import ( "net/http" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hallo Proserve!\n")) }) http.ListenAndServe(":8000", nil) }
> go build Webserver_Proverbs.go > ./Webserver_Proverbs & [1] 274944 > jobs [1]+ Läuft ./Webserver_Proverbs & > curl localhost:8000 Hallo Proserve! > killall Webserver_Proverbs > jobs [1]+ Beendet ./Webserver_Proverbs
Dieses Programm werden wir im folgenden erweitern…
Ersten Plain-Text-API-Endpoint umsetzen
- Webserver_Proverbs.go
package main import ( "fmt" "net/http" "github.com/jboursiquot/go-proverbs" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hallo Proserve!")) }) // Plain Text Endpunkt http.HandleFunc("/text", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, proverbs.Random().Saying) }) http.ListenAndServe(":8000", nil) }
> curl localhost:8000/ ; echo Hallo Proserve! > curl localhost:8000/text ; echo Make the zero value useful.
JSON-Endpoint
- Webserver_Proverbs.go
package main import ( "fmt" "net/http" "github.com/jboursiquot/go-proverbs" "encoding/json" ) type Proverb struct { Saying string `json:"saying"` Link string `json:"link"` } func HandleProverbJson(w http.ResponseWriter, r *http.Request) { randomProverb := proverbs.Random() p := Proverb { Saying: randomProverb.Saying, Link: randomProverb.Link, } err := json.NewEncoder(w).Encode(p) if err != nil { http.Error(w, "uups, da ist etwas schief gegangen!", http.StatusInternalServerError) } } func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hallo Proserve!")) }) // Plain Text Endpunkt http.HandleFunc("/text", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, proverbs.Random().Saying) }) // JSON Endpunkt http.HandleFunc("/api", HandleProverbJson) http.ListenAndServe(":8000", nil) }
> curl localhost:8000/api ; echo {"saying":"interface{} says nothing.","link":"https://www.youtube.com/watch?v=PAAkCSZUG1c\u0026t=7m36s"}
Unit-Test: JSON-Endpoint
- Webserver_Proverbs_test.go
package main import ( "net/http" "net/http/httptest" "testing" ) type TestHandleProverbJson(t *testing.T) { // 1. HTTP Recorder erstellen recorder := httptest.NewRecorder() // 2. HTTP Request req, _ := http.NewRequest)"GET", "/api", nil) // 3. HTTP-Händler aufrufen HandleProverbJson(recorder, req) // 4. Ergebnis prüfen if recorder.Code != http.StatusOK { t.Errorf("fehlerhafter Status %v, erwarteter Status %v", recorder.Code, http.StatusOK) } }
Fehler behandeln
- Webserver_Proverbs.go
package main import ( "fmt" "net/http" "github.com/jboursiquot/go-proverbs" "encoding/json" "log" ) type Proverb struct { Saying string `json:"saying"` Link string `json:"link"` } func HandleProverbJson(w http.ResponseWriter, r *http.Request) { randomProverb := proverbs.Random() p := Proverb { Saying: randomProverb.Saying, Link: randomProverb.Link, } err := json.NewEncoder(w).Encode(p) if err != nil { http.Error(w, "uups, da ist etwas schief gegangen!", http.StatusInternalServerError) } } func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hallo Proserve!")) }) // Plain Text Endpunkt http.HandleFunc("/text", func(w http.ResponseWriter, r *http.Request) { _, err := fmt.Fprintf(w, proverbs.Random().Saying) if err != nil { http.Error(w, "uups, da ist etwas schief gegangen!", http.StatusInternalServerError) } }) // JSON Endpunkt http.HandleFunc("/api", HandleProverbJson) err := http.ListenAndServe(":8000", nil) if err != nil { log.Fatal(err) } }
> ./Webserver_Proverbs & [1] 293523 > ./Webserver_Proverbs 2026/01/27 16:32:12 listen tcp :8000: bind: address already in use > killall Webserver_Proverbs
HTML-Seite für Proverbs
- Webserver_Proverbs.go
package main import ( "fmt" "net/http" "github.com/jboursiquot/go-proverbs" "encoding/json" "log" "html/template" ) type Proverb struct { Saying string `json:"saying"` Link string `json:"link"` } func HandleProverbJson(w http.ResponseWriter, r *http.Request) { randomProverb := proverbs.Random() p := Proverb { Saying: randomProverb.Saying, Link: randomProverb.Link, } err := json.NewEncoder(w).Encode(p) if err != nil { http.Error(w, "uups, da ist etwas schief gegangen!", http.StatusInternalServerError) } } func HandleProverbPage(w http.ResponseWriter, r *http.Request) { t, _ := template.New("page").Parse(`<html> <head></head> <body> <h1>Your Proverb: <a href="{{ .Link }}">{{ .Saying }}</a></h1> </body> </html>`) err := t.Execute(w, proverbs.Random()) if err != nil { http.Error(w, "uups, da ist etwas schief gegangen!", http.StatusInternalServerError) } } func main() { http.HandleFunc("/", HandleProverbPage) // Plain Text Endpunkt http.HandleFunc("/text", func(w http.ResponseWriter, r *http.Request) { _, err := fmt.Fprintf(w, proverbs.Random().Saying) if err != nil { http.Error(w, "uups, da ist etwas schief gegangen!", http.StatusInternalServerError) } }) // JSON Endpunkt http.HandleFunc("/api", HandleProverbJson) err := http.ListenAndServe(":8000", nil) if err != nil { log.Fatal(err) } }
> ./Webserver_Proverbs & [1] 296083 > curl localhost:8000/ ; echo <html> <head></head> <body> <h1>Your Proverb: <a href="https://www.youtube.com/watch?v=PAAkCSZUG1c&t=5m17s">The bigger the interface, the weaker the abstraction.</a></h1> </body> </html> > killall Webserver_Proverbs
Das Makefile
- Makefile
BINARY = "Webserver_Proverbs" clean: rm -f ${BINARY} build: clean go build -o ${BINARY} .
Das Zen von Go
- einfach
- Go erreicht viel mit wenig. Ein Programm sollte eine Sache tun, und die gut.
- bescheiden
- Unser Kode soll klar, idiomatisch und verständlich sein und seinen Zweck gut erfüllen. Clever ist da hinderlich.
- Handeln durch Nichthandeln
- Probleme eliminieren und Umwege vermeiden. Bewusstes Nichthandeln als Alternative zur Problemlösung und Umsetzung nutzen.
Ideen für erste Projekte
- Go eignet sich besonders für…
- Microservices
- Serverless Funktions
- Kommandozeilen-Tools
- DevOps und Cloud-Automatisierung
