Praktische Einführung in Go

20.10.2020 Raffael Schneider
Cloud Tech Go Microservices Hands-on Open source

Go ist eine beliebte Sprache im Cloud-Umfeld. Viele bekannte Anwendungen, sind aus der Cloud nicht mehr wegzudenken, wie beispielsweise Docker, Kubernetes, Istio oder auch Terraform, wurden in Go geschrieben. Dies bezeugt auch Steve Francia, Product-Owner von Go bei Google, in einem Interview aus dem Jahr 2019 worin er eine bewusste Ausrichtung von Go in das Cloud-Umfeld thematisiert und auch das neu entwickelte Go Cloud Development Kit vorstellt. Es scheint somit angebracht sich die Lingua franca mal etwas genauer anzuschauen.

First things first

Go (auch Golang genannt) ist eine kompilierbare Sprache, die Nebenläufigkeit (Concurrency) unterstützt und über eine automatische Speicherbereinigung (Garbage collection) verfügt. Zudem ist der Sprachsyntax minimalistisch und orientiert sich am Hardware-nahen C. Die Go Programming Language Specification ist gerade mal 50-Seiten lang. Die Idee bei Go ist eine kleinstmögliche Anzahl an einfachen, orthogonalen Instruktionen bereitzustellen, die sich in eine überschaubare Anzahl von Patterns zusammenbauen lassen. Dies mindert die Gesamtkomplexität. Somit ist es einfacher Code zu schreiben, zu verstehen und zu warten, da es oft nur einen bestimmten Weg gibt.

Die Features von Go ergeben eine Sprache die sich besonders gut für skalierbare, cluster-fähige Applikationen eignen, die darauf ausgelegt sind genau eine Aufgabe besonders gut und effizient zu lösen. Der Go-Compiler generiert kleine Binärartefakte die kleine Footprints von Docker-Images garantieren. Ausserdem stellt das Go Cloud Development Kit eine Library bereit die gängige, Cloud-typische Operationen wie das Lesen von Blob Storage (AWS S3) oder Health-Checks abdeckt. Aus diesen Gründen eignet sich Go für Microservices.

Snippet unter der Lupe

Um gleich ein Gefühl für die Sprache zu bekommen, lassen Sie uns ein zwei kurze Code-Beispiele unter die Lupe nehmen, die die Features von Golang veranschaulichen. Beim ersten Beispiel geht es um eine einfache objekt-orientierte Klasse, die Deklaration von einem Datentyp und Funktionen aufzeigt. Das zweite Beispiel zeigt auf, wie man Nebenläufigkeit in Golang mit Goroutines und Channels benutzt. Die Snippets enthalten Kommentare inline die den Ablauf erklären.

Eine einfache objekt-orientiere Klasse

Das folgende Snippet implementiert einen simplen, abstrakten Datentyp Stack im Package collection mit Golang:

// Dies ist ein Kommentar. Das Snippet ist Teil von der Klasse 'collection'
package collection

// Der Wert Null von Stack ist eine leeres Array 'data'
type Stack struct {
    data []string
}

// Die Funktion Push() addiert 'x' in das Array 'data' im Stack 's'
func (s *Stack) Push(x string) {
    s.data = append(s.data, x)
}

// Die Funktion Pop() entfernt das oberste Element im Array 'data' im Stack 's'
func (s *Stack) Pop() string {
    n := len(s.data) - 1
    res := s.data[n]
    // nötig um einen Memory-Leak zu vermeiden
    s.data[n] = ""
    s.data = s.data[:n]
    return res
}

// Die Funktion Size() gibt die Anzahl von Element im Array 'data' im Stack 's' zurück
func (s *Stack) Size() int {
    return len(s.data)
}
Implementation von Goroutines und Channels

Die Nebenläufigkeit bei Go beruht auf Goroutines und Channels. Eine Goroutine ist ein paralleler Thread. Channels sind Verbindungen die Goroutines miteinander kommunizieren lassen. Damit kann man gewisse Teile eines Programms nebeneinander und ab anderen Punkten synchron laufen lassen.

Das folgende Snippet schreibt konstant "ping" aus. Die Funktion pinger() schreibt "ping" in die Channel-Variable c. Die Funktion printer() gibt all Sekunde den Inhalt von c über eine Zwischenvariable msg aus. Mit Enterkann man die konstante Ausgabe stoppen.

package main

import (
  "fmt"
  "time"
)

func pinger(c chan string) {
  for i := 0; ; i++ {
    c <- "ping"
  }
}

func printer(c chan string) {
  for {
    msg := <- c
    fmt.Println(msg)
    time.Sleep(time.Second * 1)
  }
}

func main() {
  var c chan string = make(chan string)

  go pinger(c)
  go printer(c)

  var input string
  fmt.Scanln(&input)
}

Der <--Operator sendet Werte in die Channel-Variable c. Per := <--Operator wird der Wert aus der Channel-Variable wieder ausgelesen. Dieser Wert ist Routine-aware und ist somit zeitlich immer mit dem aktuellen Wert synchron.

Microservice mit Go

Die Basics von Go sind erläutert, jetzt lassen Sie uns die Hände ein wenig schmutzig machen. Ein einfacher Microservice stellt üblicherweise eine Reihe von Daten über eine API per REST-Schnitstelle bereit. In unserem Fall wird ein Array von Kaktus-Objekten []Cactus über die Schnittstelle bereitgestellt.

Requirements

Um das folgende Hands-on durchzuführen, werden folgende Tools vorausgesetzt:

  • Go-Compiler installiert
  • Editor (IntelliJ, VSCode, Atom, vim, ...)
  • Postman um die API zu testen
  • 5 Minuten Ihrer Zeit

Resolve zuerst die externe Library gorilla/mux mit folgendem Befehl:

$ go get -u github.com/gorilla/mux

Erstellen Sie danach eine Datei mit den Namen cactusService.go in einem beliebigen Verzeichnis. Die go-Datei muss folgenden Inhalt haben:

package main

import (
	"encoding/json"
	"log"
	"net/http"
	"strconv"

	"github.com/gorilla/mux"
)

type Cactus struct {
	ID          string `json:"id"`
	ImageNumber string `json:"imageNumber"`
	Name        string `json:"name"`
	Features    string `json:"features"`
}

var cacti []Cactus

func getCacti(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(cacti)
}

func getCactus(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	params := mux.Vars(r)
	for _, item := range cacti {
		if item.ID == params["id"] {
			json.NewEncoder(w).Encode(item)
			return
		}
	}
}

func createCactus(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	var newCactus Cactus
	json.NewDecoder(r.Body).Decode(&newCactus)
	newCactus.ID = strconv.Itoa(len(cacti) + 1)
	cacti = append(cacti, newCactus)
	json.NewEncoder(w).Encode(newCactus)
}

func updateCactus(w http.ResponseWriter, r *http.Request) {
	// tbd
}

func deleteCactus(w http.ResponseWriter, r *http.Request) {
	// tbd
}

func main() {
	cacti = append(cacti, Cactus{ID: "1", ImageNumber: "8", Name: "San Pedro Cactus", Features: "small, middle-sized,
	 tasty, red"}, Cactus{ID: "2", ImageNumber: "6", Name: "Palo Alto Cactus", Features: "big, tall, juicy, green"})

	router := mux.NewRouter()

	router.HandleFunc("/cactus", getCacti).Methods("GET")
	router.HandleFunc("/cactus", createCactus).Methods("POST")
	router.HandleFunc("/cactus/{id}", getCactus).Methods("GET")
	router.HandleFunc("/cactus/{id}", updateCactus).Methods("POST")
	router.HandleFunc("/cactus/{id}", deleteCactus).Methods("DELETE")

	log.Fatal(http.ListenAndServe(":5000", router))
}

Jetzt kann die go-Datei wie folgt kompiliert werden. Führen Sie dazu den Befehl im gleichen Verzeichnis aus:

$ go build -o cactusService

Das daraus resultierende Binary ist 6.7 MB gross. Jetzt kann unter localhost:5000/cactus/ die REST-Schnittstelle aufgerufen werden. Dazu kann Postman genutzt werden, um komfortabel die Abfrage-Parameter einzustellen.

Glückwunsch, Ihr erster Microservice in Go ist funktional! Jetzt gilt es dieses Wissen weiter zu vertiefen und einen entsprechenden Use-Case bei Ihnen im Betrieb zu erzielen. Mit Go lassen sich wartbare, prägnante Services schreiben die mit kleinem Footprints und den Best Breeds der Cloud-Welt trumpfen.

Weiterführende Links:

Web-Frameworks

Buffalo | A Go web development eco-system, designed to make your life easier

Revel Framework | A high productivity, full-stack web framework for the Go language

Lernmaterial zu Go

Educative | Getting started with Golang: a tutorial for beginners

Golang | How to Write Go Code

Raffael Schneider – Crafter, Disruptor, Freigeist. Als begeisteter GitOps-Enthusiast schreibt Raffael gerne über Programmiersprachen, Themen rund um DevOps und hat ein Faible für die neusten IT-Hypes aller Art.