image

image

Andreas Schröpfer ist seit über zehn Jahren in der IT-Beratung tätig und seit 2015 begeisterter Gopher. Er ist Contributor bei mehreren Open-Source-Projekten – darunter Go Buffalo. Er gibt Workshops zu Go, ist Mentor bei excercism.io und unterrichtet auch auf Udemy.

image

Zu diesem Buch – sowie zu vielen weiteren dpunkt.büchern – können Sie auch das entsprechende E-Book im PDF-Format herunterladen. Werden Sie dazu einfach Mitglied bei dpunkt.plus+:

www.dpunkt.plus

Andreas Schröpfer

Go –
Das Praxisbuch

Einstieg in Go und das Go-Ökosystem

image

Andreas Schröpfer

Lektorat: Melanie Feldmann

Bibliografische Information der Deutschen Nationalbibliothek

ISBN:

1. Auflage 2020

image

Hinweis:

Schreiben Sie uns:

Die vorliegende Publikation ist urheberrechtlich geschützt. Alle Rechte vorbehalten. Die Verwendung der Texte und Abbildungen, auch auszugsweise, ist ohne die schriftliche Zustimmung des Verlags urheberrechtswidrig und daher strafbar. Dies gilt insbesondere für die Vervielfältigung, Übersetzung oder die Verwendung in elektronischen Systemen.

5 4 3 2 1 0

Vorwort

Als ich Ende 2012 das erste mal Go ausprobierte, war ich auf der Suche nach einer Alternative zu meinen Python-Skripten. Diese halfen mir bisher dabei, dass meine Programme auf unterschiedlichen Systemen liefen. Ich wollte aber nichts mehr installieren müssen. Für dieses Szenario war Go ideal und ich fing an, mich mehr und mehr mit Go zu beschäftigen. Besonders die einfach Syntax half mir dabei, schnell Fortschritte zu machen. Bei Fragen fand ich immer sehr schnell Hilfe in der Community. Vor allem der freundliche und geduldige Umgang miteinander haben mich dabei besonders beeindruckt. Das hat mich auch dazu ermutigt, anderen Menschen Go beizubringen. Neben kleinen Workshops und Vorträgen hatte ich mich auch entschlossen Udemy-Kurse zu erstellen, um meine Reichweite zu erhöhen. Als ich dann vom dpunkt.verlag gefragt wurde, ob ich über Go auch ein Buch schreiben würde, musste ich nicht lange zögern. Das Ergebnis siehst Du hier!

Dieses Buch ist eine Mischung aus Theorie und mehreren kleinen Projekten. Mit den Projekten möchte ich Dich an die Hand nehmen und Dir zeigen, wie Du verschiedene Aufgaben mit Go lösen kannst. Die Projekte sind so gewählt, dass Du sie schnell eigenständig nachprogrammiert kannst. Ich würde jedem empfehlen, die Beispiele einmal selbst abzutippen.

Alle Ungeduldigen können den Code von GitHub herunterladen. Das Repository hat folgende URL: https://github.com/gobuch/code

Das Maskottchen von Go ist der Gopher. Dieser taucht hin und wieder in den Kapiteln auf. Die Abbildungen wurden von mir nach einem Vorbild von Renee French erstellt.

Das Buch zu konzipieren und zu schreiben hat viel Zeit eingenommen, deshalb möchte im mich ganz besonders bei meiner Familie bedanken, die mir diese Zeit gegeben hat. Ein großes Dankeschön geht auch an Alex und Marcel für das technische Feedback. Ein ganz besonderer Dank geht an meine Lektorin Melanie Feldman, ohne die dieses Buch sicher gar nicht zustande gekommen wäre.

Troisdorf im März 2020

Andreas Schröpfer

Inhaltsverzeichnis

Vorwort

1Einleitung

1.1Ziel dieses Buches

1.2Die Geschichte von Go

1.3Installation

1.4Sicherheit und Patches

1.5Editoren für den Go-Werkzeugkasten

1.6Der Spielplatz für Gopher

1.7Hello World

1.8Eine lesbare technische Spezifikation

1.9Ausgabe mit dem fmt-Paket

2Vorstellung der Syntax

2.1Wörter, Funktionen und Typen von Go

2.2Variablen

2.3Konstanten

2.4Pointer

2.5Eigene Typen

2.6Typumwandlung

2.7Zusammengesetzte Strukturen

2.8Funktionen

2.9Objektorientierung mit Methode

2.10Exportiert und nicht exportiert

2.11Arrays

2.12Slices

2.13Das Slice als Pointer

2.14Maps

2.15if

2.16switch

2.17for

2.18Labels und goto

2.19Blank Identifier

2.20UTF-8, strings und runes

3Projekt: Command Line Interface

3.1Einleitung

3.2gocat – File-Ausgabe

3.3Den md5-Hash erzeugen

3.4Dateien und HTTP-Server als Quellen für gomd5

4Go Tooling

4.1Schnelle Hilfe mit go help

4.2Kompilieren und Installieren

4.3Umgebungsvariablen mit go env

4.4Ein Programm für jede Gelegenheit – Build Tags

4.5Wie Code formatiert wird – gofmt

4.6Automatische Imports mit goimports

4.7Dokumentation immer dabei – godoc

5Projekt: Ein einfacher Webloader

5.1Einleitung

5.2CLI – unser Interface

5.3HTTP-Request erstellen

5.4Implementierung des File-Outputs

5.5Ausgabe des HTTP-Headers

5.6Gültigkeit der übergebenen URL

6Eigene Pakete und Module

6.1Go-Code lebt in Paketen

6.2Paketnamen

6.3Die init()-Funktion

6.4Semantic Versioning

6.5Pakete leben in Modulen

6.6Der Workflow, seit es Module gibt

6.7Neuer bedeutet nicht immer besser

6.8Update unserer Abhängigkeit

6.9Neue Major-Version mit Modulen

7Projekt: Code generieren

7.1Einleitung

7.2Ein Tool, um Code zu generieren

7.3Template erstellen

7.4Anwenden von go generate

8Concurrency-Grundlagen

8.1Concurrency mit Go

8.2Parallelität im echten Leben

8.3Goroutinen

8.4Channels

8.5Einen Channel schließen

8.6Select

8.7Race Conditions und Data Races

9Concurrency Patterns

9.1Checkliste zu Goroutinen

9.2Goroutinen melden, wenn sie fertig sind

9.3Beenden von Goroutinen

9.4Context

9.5Prüfung eines geschlossenen Channels

9.6Pipelines

9.7Generator

9.8Fan-In und Fan-Out

9.9Channel of Channels

9.10Worker Pool

9.11Semaphore mit einem Buffered Channel

9.12State Machine

10Projekt: Go Concurrency

10.1Einleitung

10.2Command Line Interface

10.3Argumente parsen

10.4Befehle ausführen

10.5Abbruch mit context

10.6Verbesserung des Tools

11Testen und Benchmarks

11.1Tests in Go

11.2Subtests

11.3Tabellarische Tests

11.4Eigenes Testpaket

11.5Testen mit Beispielen

11.6Ein ganzes Projekt testen

11.7Benchmarks

11.8Syntax der Benchmarks

11.9Subbenchmarks

12Projekt: Image Resizer

12.1Einleitung

12.2Command Line Interface – Erstellen der Flags

12.3Größe erzeugen

12.4Bild verkleinern

12.5Filename prüfen

12.6Funktionen zusammenführen

12.7Refactoring in eine zusätzliche Funktion

12.8Eigener Fehlertyp

12.9Von sequentieller Ausführung zu nebenläufiger Ausführung

13Interfaces

13.1Bessere Abstraktion mit Interfaces

13.2Die richtige Interface-Erstellung

13.3Interne Abbildung der Interface-Typen

13.4Leeres Interface

13.5Vom Interface zum konkreten Typ

13.6Interface in andere Interfaces einbinden

13.7Interfaces in Strukturen einbinden

13.8Mocking und Tests mit io.Reader und io.Writer

14Projekt: Kopieren mit Reflection

14.1Einleitung

14.2Reflection in Go

14.3Beschreibung des Pakets

14.4Testfälle für unser Paket

14.5Umsetzung

14.6Verwenden von Tags

15Fehlerbehandlung

15.1Grundlagen

15.2Variablen und Konstanten

15.3Eigene Fehlertypen

15.4Einem Fehler Kontext hinzufügen

15.5Keine Panik

16Projekt: Ein einfacher Webserver

16.1Einleitung

16.2Das Modell für unseren Blog

16.3Der Webserver und seine Handler

16.4Templates erstellen

16.5Kommentarfunktion

16.6Files ausliefern

16.7API bereitstellen

16.8Template nur einmal parsen

16.9Nebenläufiger Job für den Index

16.10Ein paar kleine Verbesserungen

1Einleitung

image

1.1Ziel dieses Buches

Willkommen in diesem Buch über die Programmiersprache Go. Hier wirst Du mit praktischen Beispielen und Übungen lernen, wie Du Programme in dieser Sprache erstellt. Zu Beginn der theoretischen Kapitel stehen ein paar Fragen, die Dir bereits verraten, welche Inhalte wichtig sind. In diesem allerersten Kapitel werde ich folgende Fragen beantworten:

In den folgenden Kapiteln möchte ich Dir die Programmiersprache Go vorstellen. Wir beginnen mit der Syntax und ein wenig allgemeiner Theorie. Damit das alles nicht zu trocken wird, gibt es zwischen den theoretischen Kapiteln kleine Projekte. Am Ende des Buches weißt Du, wie Du skalierbaren und nebenläufigen Code mit Go schreibst, wie Du die Dokumentation nutzt und wie Du eigene Pakete und Tools erstellen kannst.

Umfangreiche Werkzeuge erleichtern die Arbeit mit Go.

Go ist eine sehr junge Programmiersprache, zumindest verglichen mit C, C++ oder Java. Die Syntax der Sprache ist überschaubar klein und sehr gut dokumentiert. Außerdem hat das Go-Team neben der Sprache auch gleich noch einen großen Werkzeugkasten angelegt, der wirklich alles beinhaltet, was für professionelles Coding notwendig ist: Dokumentation, Test-Framework, statische Codeanalyse, Race Detector und vieles mehr. Alles ist direkt bei der Go-Installation dabei und steht über eine Open-Source-Lizenz jedermann frei zur Verfügung.

Go macht Spaß.

Dieses Zusammenspiel aus großer Einfachheit und frei zugänglichen Tools hat mich von der Sprache vollends überzeugt. Es macht sehr viel Spaß, Programme in Go zu schreiben und diesen Spaß auch an andere weiterzugeben. Genau diesen Enthusiasmus möchte ich mit diesem Buch vermitteln.

1.2Die Geschichte von Go

2007 bei Google

Die Sprache Go wurde 2007 bei Google entworfen. Die Personen hinter der Sprache sind Robert Griesemer, Rob Pike und Ken Thompson. Angeblich soll Go während des Kompilierens von C++-Programmen entstanden sein. Ganz so stimmt das nicht, doch die langen Kompilierzeiten waren ein wichtiger Auslöser für das Go-Projekt. In einem Interview hat Rob Pike einmal geschildert, dass er an einem komplexen Stück Code arbeitete und nach jeder Änderung 45 Minuten warten musste, bis das dazugehörige Programm kompiliert war. Ein weiterer Auslöser für Go war die schlechte Unterstützung für Multi-Core-Prozessoren der anderen Programmiersprachen. Denn Rob Pike hatte durch andere Projekte, z. B. Newsqueak, auf dem Gebiet der nebenläufigen Programmierung bereits einige Erfahrung gesammelt.

Über die Macher

Rob Pike und Ken Thompson waren bereits vor Go wichtige Namen der IT-Geschichte. Rob Pike ist z. B. einer der Köpfe hinter UTF-8 und Ken Thompson hatte seinerzeit die Sprache B entwickelt, den Vorgänger von C. Der Dritte im Bunde, Robert Griesemer, ist auch kein unbeschriebenes Blatt. Er arbeitete damals z. B. an der Optimierung der JavaScript-Engine V8.

Nach der initialen Zündung für Go konnten die drei Google davon überzeugen, dass es sinnvoll ist, Go als Sprache für Cloud-Server zu entwickeln. Wenn wir uns in späteren Kapiteln an manchen Stellen fragen, warum gewisse Features so in der Standardbibliothek implementiert sind und nicht anders, dann liegt das am ursprünglichen Verwendungszweck als Serversprache für Google.

Google hatte 2018 knapp 100.000 Mitarbeiter, wovon geschätzt ca. 60 % Entwickler waren. Bei einer so großen Anzahl von Entwicklern ergeben sich mehrere Probleme. Code wird in großen Organisationen nämlich mehr gelesen als geschrieben. Außerdem kommen bei Google auch laufend viele neue Entwickler ohne große Erfahrung hinzu. Deshalb sollte die neue Programmiersprache Go einfach zu erlernen und einfach zu lesen sein.

Go soll schnell zu lernen sein.

Die Syntax von Go ist deswegen an C und Java angelehnt. Dadurch können Entwickler mit Erfahrung in diesen Sprachen Go einfacher lernen. Um die Sprache so einfach wie möglich zu gestalten, wurde nur das Notwendigste in die Sprache aufgenommen. Go erfindet das Rad nicht neu, sondern ist wie C oder Java, aber eben auf das Notwendigste reduziert.

Seit 2009 ist Go Open Source.

Seit 2009 sind die Sprache und die dazugehörigen Werkzeuge Open Source und können somit kostenlos sowohl privat als auch professionell genutzt werden. Seit diesem Zeitpunkt gibt es eine aktive weltweite Community, die gemeinsam Go weiterentwickelt.

2012 erfolgte die Veröffentlichung der Version 1. Damit einher ging auch das Versprechen, dass es keine Breaking Changes mit einem neuen Release gibt. Wenn der Code unter 1.10 läuft, dann wird der auch unter 1.15 laufen. Dieses Versprechen gilt sowohl für die Syntax als auch für die Standardbibliothek. Es wird sehr ernst genommen und ist damit auch ein Garant für die Stabilität der Sprache.

1.3Installation

Die Installation der Go-Umgebung ist abhängig vom jeweiligen System. Unter https://golang.org/doc/install sind die Installationsanleitungen und die notwendigen Vorraussetzungen für alle System aufgeführt. Eine Go-Installation beinhaltet den Compiler, das Go Tooling (siehe Kapitel 4) und die Standardbibliothek. Für macOS und Windows gibt es bereits vorgefertigte Installer, die bei der Installation alle notwendigen Umgebungsvariablen setzen. Für Linux müssen diese selbst gesetzt werden. Die dafür notwendigen Befehle befinden sich auch auf der Seite.

Achtung: Version prüfen!

In diesem Buch sind die Beispiele und Projekte so aufgebaut, dass sie als Modul initialisiert werden. Go unterstützt dies jedoch erst seit der Version 1.11. Wir müssen also sicherstellen, dass auf Deinem System auch eine Version höher 1.11 installiert ist. Mit dem Befehl go version kannst Du die installierte Go-Version auf Deinem System prüfen.

Sollte auf Deinem System 1.10 oder niedriger vorhanden sein, empfehle ich, eine neuere Version zu installieren. Insbesondere bei manchen konservativen Linux-Distributionen kann es sein, dass über die Paketverwaltung im Standard nur ältere Versionen von Go vorhanden sind. Lokal zum Ausprobieren ist eine veraltete Version in Ordnung. Doch spätestens, wenn wir Programme für einen produktiven Betrieb kompilieren, ist es wichtig, immer die aktuellste Version zu besitzen.

1.4Sicherheit und Patches

Support ist gewährleistet.

Das Thema Sicherheit ist für die produktive Nutzung ein entscheidender Aspekt. Denn sobald Sicherheitslücken entdeckt werden, müssen diese so schnell wie möglich geschlossen werden. Da z. B. Docker und Kubernetes mit Go laufen und dies so ziemlich in fast allen Cloud-Diensten eingesetzt werden, sind sehr viele Firmen daran interessiert, auftretende Sicherheitslücken so schnell wie möglich zu beheben. Deswegen sorgt das Go-Team für schnelle Updates und regelmäßige Patches.

Da wir Sicherheitslücken nicht öffentlich als Bug-Report melden sollen, würden wir eine Mail an security@golang.org schicken. Der Public Key für eine PGP-Verschlüsselung findet sich auf der Webseite https://golang.org/security. Dort ist auch der gesamte weitere Prozess beschrieben.

ein Jahr Support

Sicherheitsrelevante Updates werden dann für die zwei aktuellsten Go-Versionen veröffentlicht. Der Support für die Version 1.12 endet mit dem Release von 1.14. Der Releasezyklus für Go ist halbjährlich, wodurch die aktuelle Version somit immer ein Jahr lang unterstützt wird.

1.5Editoren für den Go-Werkzeugkasten

Seitdem Go 2009 als Open-Source-Projekt für die Allgemeinheit freigegeben wurde, ist die Popularität der Sprache konstant gestiegen. Im letzten Quatal 2019 lag Go auf Platz 4 aller Pull-Requests von GitHub1. Damit verbunden ist eine immer bessere Unterstützung durch alle gängigen Entwicklungsumgebungen und Editoren.

Einbindung der Werkzeuge

Es gibt sehr gute Erweiterungen für alle gängigen Editoren wie Vim, Emacs, Eclipse oder Sublime. Eine aktuelle Liste aller Editoren mit den dazugehörigen Erweiterungen wird im Go-Wiki unter https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins aufgelistet. Wenn Du also einen bestimmten Editor favorisiert, findest Du dort Informationen bezüglich der Go-Unterstützung. Da Go selbst mit einem großen und mächtigen Werkzeugkasten ausgeliefert wird, ist es sinnvoll, für die Entwicklung in Go auch eine Erweiterung zu benutzen. Das ist beim Coden unglaublich hilfreich. So können wir unseren Code beim Speichern formatieren oder die Importe automatisch ergänzen lassen.

Visual Studio Code

Ich persönlich habe ausgezeichnete Erfahrungen mit Visual Studio Code von Microsoft gemacht. Dieser kostenlose Editor besitzt eine sehr gute Unterstützung für Go und ist für alle Betriebssysteme verfügbar. Seit 2019 gibt es mit gopls auch einen offiziellen Language Server für Go. Durch diesen Server können Editoren die gleichen Funktionalitäten wie Auto-Vervollständigung, Anzeige der Dokumentation oder Springen zur Definition verwenden. Die Unterstützung ist (Stand Ende 2019) noch im experimentellen Stadium und wird laufend ausgebaut.

1.6Der Spielplatz für Gopher

Der Go Playground ist eine Webseite, die es uns ermöglicht, kleine Code-Schnipsel auszuprobieren. Die Seite ist über die URL https://play.golang.org/ erreichbar. Wenn wir die Seite aufrufen, gibt es dort bereits ein paar Zeilen Code, die unserem Hello-World-Programm aus Kapitel 1.7 sehr stark ähneln.

image

Abb. 1–1 Go Playground

Im Playground können wir Code ausführen lassen. Dies ist immer dann hilfreich, wenn wir kleine Sachen schnell und unkompliziert ausprobieren wollen.

Die Seite ist einfach aufgebaut. Oben gibt es die Buttons Run, Format, Imports und Share. Mit Run führen wir den Code aus. Mit Format wird das Tool gofmt ausgeführt, das den Code einheitlich formatiert. Mehr dazu gibt es in Kapitel 4.5. Wenn bei Imports ein Häkchen gesetzt ist, wird unter dem Button Format nicht gofmt, sondern goimports ausgeführt, und es werden zusätzlich alle fehlenden Importe automatisch ergänzt. Denn intern ruft goimports auch den Befehl gofmt auf. Über Share wird der Code gespeichert und eine URL angezeigt. Über diese URL lässt sich der gespeicherte Code wieder aufrufen. Diese Funktion ist hilfreich, wenn wir mal ein Problem haben. So können wir das Problem im Playground nachstellen und dann per Mail, Forum, Stack Overflow oder Slack teilen.

1.7Hello World

Beginnen wir unsere Reise in die Go-Welt mit Hello, World!. Dafür erstellen wir uns ein neues Verzeichnis hello-world. Anschließend wechseln wir in das neue Verzeichnis. Nun müssen wir unser Projekt initialisieren. Dafür benötigt Go eine weltweit eindeutige Identifikation. Das klingt erst mal kompliziert, ist aber eigentlich immer die URL für das Repository. Also die URL, die wir auch bei git clone verwenden.

go mod init github.com/deinUser/hello-world

go: creating new go.mod: module github.com/deinUser/hello-world

Wenn wir unser Projekt nur lokal verwenden, können wir auch eine fiktive URL wie local/meinModul für die Initialisierung verwenden.

Mit dem init-Befehl initialisieren wir das Go-Projekt auf unserem Rechner. Dabei wird die Datei go.mod angelegt. Dort ist vorerst nur die Definition unseres Projektes enthalten. In Kapitel 6.9 beschäftigen wir uns dann näher mit dem Thema Module und welche Informationen noch in dieser Datei vorhanden sein können. Anschließend erzeugen wir eine Datei mit dem Namen main.go in unserem hello-world-Verzeichnis.

package main

import "fmt"

func main() {

fmt.Println("Hello, World!")

}

Listing 1–1 Hello World

Gehen wir als Erstes unseren Code Zeile für Zeile durch. Ganz am Anfang geben wir mit package an, zu welchem Paket diese Datei gehört. Wenn wir ein ausführbares Programm erstellen, dann ist der Einstieg immer das Paket main.

Mit import müssen wir alle im Code verwendeten Pakete angeben. Der Go-Compiler ist streng und prüft auch, ob alle aufgeführten Abhängigkeiten im Code verwendet werden. Die Prüfung erfolgt dabei pro Datei. Nicht verwendete Importe erzeugen einen harten Fehler und führen dazu, dass sich das Programm nicht kompilieren lässt.

In Go setzen wir die Importe so gut wie nie selbst. Denn das Programm goimports erledigt diese Aufgabe für uns. Neben den Importen wird dabei auch automatisch der Code einheitlich formatiert. Die meisten Editoren rufen dieses Programm nach jedem Speichern auf. Solltest Du bereits Deine Entwicklungsumgebung für Go aufgesetzt haben, dann kannst Du einfach mal die Importe aus dem Code entfernen und die Datei speichern. Die Importe sollten kurz nach dem Speichern wieder erscheinen.

Namespace der Pakete

Aber gehen wir zurück zu unserem Code. Für die Ausgabe von Hello, World! benötigen wir das fmt-Paket. Durch die Import-Anweisung teilen wir zum einen dem Compiler mit, welche Pakete er noch benötigt, und zum anderen definiert dies in unserem Code einen Namensraum bzw. Namespace. Über diesen Namensraum können wir alle Funktionen, Typen, Variablen und Konstanten des Pakets ansprechen. In unserem Fall wäre das also immer fmt.

Als Nächstes folgt die Definition der Funktion main(). Diese Funktion ist, wie in den meisten anderen Sprachen, der Einstieg in unser Programm. Sie besteht aus fmt.Println("Hello, World!"). Damit rufen wir die Funktion Println() des Pakets fmt auf. Die Funktion selbst sendet den übergebenen String "Hello, World!" an die Standardausgabe.

Um unser Hello-World-Programm auch ausführen zu können, müssen wir es kompilieren. Das machen wir mit dem Befehl go build. Anschließend gibt es in unserem Verzeichnis die ausführbare Datei hello_world bzw. bei Windows hello_world.exe. Wenn wir diese Datei über einen Terminal ausführen, wird der String Hello, World! ausgegeben.

Wenn wir nicht den Umweg über eine kompilierte Datei gehen möchten, können wir unser Programm auch direkt mit go run main.go ausführen. Dies liefert die gleiche Ausgabe wie vorher, nur ohne kompilierte Datei. Streng genommen wird mit go run das Programm auch kompiliert, jedoch wird die Datei in einem temporären Verzeichnis gespeichert und dann ausgeführt. Nachdem das Programm durchgelaufen ist, wird alles wieder gelöscht. Da Go sehr schnell kompiliert, fühlt es sich so an, als ob wir den Code direkt ausführen würden.

1.8Eine lesbare technische Spezifikation

Bevor eine Programmiersprache tatsächlich implementiert wird, steht die Spezifikation an. Diese beschreibt das Verhalten und die Sprachsyntax. Für Go finden wir die Spezifikation unter https://golang.org/ref/spec. Im Vergleich zu anderen Spezifikationen ist diese verständlich geschrieben und mit Beispielen angereichert. Besonders für Umsteiger aus anderen Sprachen ist es sinnvoll, am Anfang immer wieder einen Blick in dieses Dokument zu werfen.

Effective Go

Auf https://golang.org/ gibt es noch viele weitere sehr hilfreiche und nützliche Ressourcen. Auch die Blog-Einträge sind für jeden Go-Neuling Gold wert. Selbst die älteren Artikel von 2010 sind heute noch relevant. Die Autoren dieser Beiträge sind erfahrene Entwickler des Go-Teams und die Artikel spiegeln auch deren großen Erfahrungsschatz wider. Darauf aufbauend gibt es Effective Go2, das beschreibt, wie wir Go-Code schreiben sollen.

image

Abb. 1–2 Ausschnitt aus der Spezifikation

1.9Ausgabe mit dem fmt-Paket

Das fmt-Paket ist uns bereits in Kapitel 1.7, bei der Ausgabe von Hello World begegnet. Der Name des Pakets ist die Kurzform von Format, und wird Fammt ausgesprochen. Die Aufgabe des Pakets ist die formatierte Aus- und Eingabe von Werten. An dieser Stelle wollen wir uns jedoch nur mit den Print-Funktionen beschäftigen, die für die Ausgabe zuständig sind. Denn diese Funktionen werden uns in den kommenden Kapiteln überall wieder begegnen.

Es gibt drei unterschiedliche Print-Funktionen:

func main() {

fmt.Print("Hallo ", "Print()\n")

fmt.Println("Hallo Println()")

var s = "Printf()"

fmt.Printf("Hallo, %s\n", s)

}

// Output:

// Hallo Print()

// Hallo Println()

// Hallo, Printf()

Listing 1–2 Die drei unterschiedlichen Print-Funktionen

Ausgabe in den Standardoutput

Alle Funktionen, die mit der Bezeichnung Print beginnen, schreiben die Werte in den Standardoutput. Das heißt, wenn wir das Programm über die Kommandozeile ausführen, werden die Werte auch dort ausgegeben. Alle Funktionen können beliebig viele Eingabewerte verarbeiten. Dabei gibt fmt.Print() die übergebenen Werte einfach aus. Die Funktion fmt.Println() funktioniert genauso, nur dass am Ende des Outputs automatisch ein Zeilenumbruch ausgegeben wird.

Die Funktion fmt.Printf() ermöglicht uns eine formatierte Ausgabe von Werten. Printf-Funktionen gibt es auch in so ziemlich allen anderen Programmiersprachen. Dabei wird ein Ausgabestring durch sogenannte Verben angereichert. Dem jeweiligen Verb wird dann eine Variable zugeordnet, deren Wert in den String eingefügt wird. Das Tolle daran ist, dass wir über das Verb die Ausgabe formatieren können.

func main() {

var nr = 2

var name = "Till"

fmt.Printf("%03d: Hallo, %s\n", nr, name)

}

// Output:

// 002: Hallo, Till

Listing 1–3 Formatierte Ausgabe mit fmt.Printf()

In unserem Beispiel ist der Ausgabestring "%03d: Hallo, %s\n". Mit dem Verb %03d geben wir eine Zahl mit drei Ziffern aus, wobei fehlende Stellen mit 0 gefüllt werden. Das Verb %s steht für die Ausgabe als String.

wichtige Verben

In unseren Beispielen und Projekten werden uns folgende Verben begegnen:

weitere Verben

Das fmt-Paket unterstützt auch noch viele weiteren Verben. Die vollständige Aufstellung aller hier möglichen Ausgabevarianten befindet sich gleich am Anfang der Dokumentation des Pakets unter https://golang.org/pkg/fmt/.

Ausgabe als String

Es kann aber auch sein, dass wir die erzeugten Strings nicht sofort ausgeben möchten. Auch dafür gibt es passende Funktionen in diesem Paket. Es gibt auch die Möglichkeit, die Werte direkt als String zu erhalten. Dafür bekommen die Print-Funktionen das Präfix S. Die Funktionen heißen somit fmt.Sprint(), fmt.Sprintln() und fmt.Sprintf().

func main() {

var nr = 2

var name = "Till"

s := fmt.Sprintf("%03d: Hallo, %s\n", nr, name)

fmt.Print(s)

}

// Output:

// 002: Hallo, Till

Listing 1–4 fmt.Sprintf() liefert einen String.

In beliebige Datenziele schreiben

Es ist auch möglich, direkt in ein Datenziel zu schreiben. Hierfür verwendet die Standardbibliothek das Interface io.Writer. Dem Thema Interfaces wenden wir uns noch ausführlich in Kapitel 13 widmen. An dieser Stelle ist vorerst nur wichtig, dass wir damit in bestimmte Datenziele direkt schreiben können. So können wir darüber entscheiden, ob wir Daten in eine Datei, als Antwort eines Webservers oder in den Standardoutput schreiben. Diese Funktionen besitzen das Präfix F. Somit sind fmt.Fprint(), fmt.Fprintln() und fmt.Fprintf() die Funktionen, für das Schreiben in bestimmte Datenziele. Als erste Variable wird dabei das Datenziel angegeben.

func main() {

file, _ := os.Create("datei.txt")

var nr = 2

var name = "Till"

fmt.Fprintf(file, "%03d: Hallo, %s\n", nr, name)

file.Close()

}

Listing 1–5 String wird in ein File geschrieben.

Das obige Beispiel funktioniert, jedoch ignorieren wir beim Erzeugen der Datei einen möglichen Fehler. Im Kapitel 15 werden wir besprechen, warum das Ignorieren von Fehlern keine gute Idee ist. Da wir hier nur die Funktionsweise von fmt.Fprintf() ausprobieren möchten, wollen wir den Code nicht unnötig aufblähen. Anstatt von file könnten wir als Datenziel hier auch os.Stdout angeben. In diesem Fall würden wir das Ergebnis direkt in den Standardoutput schreiben.

2Vorstellung der Syntax

image

2.1Wörter, Funktionen und Typen von Go

Als Einstieg in die Sprachsyntax beginnen wir mit den reservierten Wörter und vordefinierten Funktionen. Go wurde von Anfang an als C-ähnliche Sprache designt. Da C viele anderen Programmiersprachen beeinflusst hat, sollten uns die meisten Wörter bekannt vorkommen. Dieser Einstieg erlaubt uns einen ersten Überblick, was die Sprache alles zu bieten hat.

break default func interface select

case defer go map struct

chan else goto package switch

const fallthrough if range type

continue for import return var

Listing 2–1 reservierte Wörter

Neben den reservierten Wörtern gibt es vordefinierte Funktionen, die direkt in die Sprache integriert sind.

close new panic complex delete

make recover real len append

print imag cap copy println

Listing 2–2 vordefinierte Funktionen

Go besticht durch Einfachheit.

Vielleicht ist mancher jetzt ein wenig enttäuscht, dass Go gar nicht so viel zu bieten hat. Aber weniger ist in diesem Sinne definitiv mehr. Denn durch diese Einfachheit ist Go leicht zu erlernen. Bei der Konstruktion der Sprache wurde auf alles verzichtet, was nicht unbedingt notwendig ist. Wo es in anderen Sprachen viele unterschiedliche Lösungsmöglichkeiten gibt, ist Go beschränkter und ermöglicht meistens nur einen Weg. Das ist auch den wenigen reservierten Wörtern und Funktionen geschuldet. Es wurde auf alle unnötigen Schnörkel verzichtet. Ein weiterer Aspekt ist die Lesbarkeit des Codes. Weil die wenigen eingebauten Funktionen meistens nur eine Lösung zulassen, sieht Go-Code fast immer gleich aus, unabhängig davon, wer diesen geschrieben hat. Das steigert die Lesbarkeit.

Als Nächstes wollen wir uns die Basistypen ansehen. Auch diese sollten uns bereits aus anderen Sprachen bekannt sein. Go geht auch hier eher den konservativen Weg.

bool string

int int8 int16 int32 int64

uint uint8 uint16 uint32 uint64 uintptr

byte rune float32 float64

complex64 complex128

Listing 2–3 Basistypen

Bezüglich der Operatoren und der Zeichensetzung unterscheidet sich Go auch nicht von anderen Sprachen. Die meisten der in der Spezifikation aufgeführten Einträge dürften uns deshalb bekannt sein.

& += &= && == != ( )

- | -= |= || < <= [ ]

* ^ *= ^= <- > >= { }

/ << /= <<= ++ = := , ;

% >> %= >>= -- ! ... . :

&^ &^=

Listing 2–4 Operatoren

Das sind die Elemente, die Go als Sprache mitbringt und es uns erlauben, daraus unsere Programme zu erstellen.

2.2Variablen

In Go werden Variablen mit dem Schlüsselwort var definiert. Die Syntax verlangt, dass der Typ immer nach dem Variablennamen angegeben wird. Wenn der Variablen gleich ein Wert zugewiesen wird, kann auch auf den Typ verzichtet werden, da der Wert den Typ bestimmt.

var nummer int // Definition ohne Wert

var name string = "Rob Pike" // Definition mit Wert und Typ

var anzahl = 10 // Definition ohne Typ

Listing 2–5 Definition von Variablen mit var

Nullwert

Wenn eine Variable ohne Wert definiert wird, besitzt diese von Anfang an einen initialen Nullwert. Wenn wir später den Wert prüfen, können wir dabei nicht mehr feststellen, ob wir diesen Wert bewusst gesetzt haben oder ob dieser seit der Initialisierung besteht.

var nummer int // 0

var txt string // ""

var checked bool // false

var meinUser *user // nil - Pointer

var liste []string // nil - Slice

Listing 2–6 Nullwerte von Variablen

Listing 2–6 enthält bereits einen Pointer und ein Slice. Hier ist der Nullwert nil. Darauf werden wir in den Kapiteln 2.4 und 2.12 eingehen.

Variablen können bei der Deklaration gruppiert werden. Diese Gruppierung macht den Code lesbarer, da wir so auf einen Blick sehen, welche Variablen thematisch zusammenpassen, wie das folgende Beispiel aus Effective Go anschaulich zeigt.

var (

home = os.Getenv("HOME")

user = os.Getenv("USER")

gopath = os.Getenv("GOPATH")

)

Listing 2–7 Definition einer Gruppe von Variablen

Wenn der Variablen gleich ein Wert zugewiesen wird, kann die sogenannte Kurzdeklaration verwendet werden. Hierfür wird vor dem Gleichheitszeichen ein Doppelpunkt gesetzt. Dadurch erfolgt die Definition des Typs der Variablen über den Wert.

nummer := 10 // int

name := getName() // string

Listing 2–8 Kurzdeklaration

In Go können auch mehrere Variablen mit einer Anweisung verarbeitet werden. Hierfür trennen wir die jeweiligen Teile mit einem Komma.

a, b := 12, 34

a, b = b, a // Werte tauschen

Listing 2–9 Mehrere Variablen in einer Anweisung

Wir müssen beachten, dass wir einmal deklarierte Variablen im Code auch verwenden müssen. Sogenannte verwaiste Variablen führen sonst zu einem Fehler beim Kompilieren. Besonders am Anfang kann dieser Umstand ganz schön nervig sein. Aber unser Code wird dank dieser Logik nicht durch unnötige Variablen zugemüllt.

Variablennamen

Für die Namen unserer Variablen gibt es ein paar Regeln, die wir beachten müssen.

öffentliche Variablen

Innerhalb dieser Regeln sind technisch alle Namen zulässig. Wenn wir eigene Pakete erstellen, ist es außerdem entscheidend, ob eine Variable mit einem großen oder einem kleinen Buchstaben beginnt. Denn darüber wird die Sichtbarkeit für andere Pakete gesteuert. Alle Bezeichner in Go, die mit einem Großbuchstaben beginnen, sind öffentlich für andere Pakete. Mehr zu diesem Thema behandelt Kapitel 2.10.

kurz ist besser

Für das Erstellen von Variablen gibt es auch eine Empfehlung: Die Länge der Variablen sollte sich nach deren Verwendung richten. Variablen, die wir nur direkt nach ihrer Deklaration verwenden oder eine begrenzte Gültigkeit haben, sollten kurz sein. Klassisch ist hier der Index. Wir schreiben lieber i anstatt index.

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

fmt.Println(i)

}

Listing 2–10 i statt index

Camel Case

Wenn Variablen über längere Abschnitte im Code verwendet werden, dann verwenden wir natürlich einen sprechenderen Namen. Dieser kann auch aus mehreren umschreibenden Wörtern bestehen, die wir mittels Camel Case zusammenfügen.

var userName string // idiomatischer Stil

var user_name string // schlechter Stil

Listing 2–11 Camel Case für zusammengesetzte Namen

Bei Abkürzungen im Namen sollten diese einheitlich komplett großgeschrieben sein.

var userURL string

var HTTPServer server // exportierte Variable

var httpServer server // nicht exportierte Variable

Listing 2–12 Abkürzungen

Die Ausnahme von der Regel sind nicht öffentliche Variablen, die mit einem Kleinbuchstaben beginnen müssen. In diesem Fall schreiben wir die komplette Abkürzung klein.

2.3Konstanten

Konstanten werden wie Variablen deklariert, jedoch verwenden wir dafür das Schlüsselwort const. Die Kurzdeklaration mit := ist bei Konstanten nicht möglich. Konstanten können auch keine Strukturen (Kapitel 2.7) aufnehmen. Sobald wir einmal eine Konstante deklariert haben, können wir diese nicht mehr ändern.

// einfache Deklaration

const a = 10

// Deklaration als Gruppe

const (

maxBreite = 100

maxLaenge = 100

)

// Deklaration über einen Ausdruck

const b = 10 + 4

Listing 2–13 Deklaration von Konstanten

Eine Besonderheit von Konstanten ist die Typbehandlung. Denn Konstanten sind in der Regel untypisiert. Das heißt, dass bei der Verwendung der Konstante versucht wird, diese in den benötigten Typ umzuwandeln. Sollte eine Typumwandlung nicht möglich sein, erzeugt dies einen Fehler beim Kompilieren.

Folgendes Beispiel zeigt, dass wir eine Konstante, die als Fließkommazahl deklariert wurde, auch als Integer verwenden können. Wenn wir statt einer Konstanten eine Variable verwenden, kommt es zu einem Fehler.

func add2(a int) int {

return 2 + a

}

func main() {

const c = 2.0

fmt.Println(add2(c))

// Output:

// 4

var v = 2.0

fmt.Println(add2(v))

// Output:

// Fehler beim Kompilieren:

// cannot use v (type float64) as type int

// in argument to add2

}

Listing 2–14 Konstanten sind untypisiert.

Wir können eine Konstante natürlich auch typisiert deklarieren. Hierfür müssen wir den Typ einfach bei der Deklaration mitgeben. Auch die Funktion der Typumwandlung (Kapitel 2.6) ist dabei zulässig.

const a float64 = 2.0

const b = float64(2.0)

Listing 2–15 typisierte Konstanten

iota: der automatische Zähler

Bei numerischen Konstanten können wir diese mit einem automatischen Zähler definieren. Hierfür benötigen wir das Schlüsselwort iota. Der Zähler beginnt bei jeder neuen Gruppe von Konstanten bei Null und zählt mit jedem Aufruf um eins hoch. Die Zuweisung von iota ist nur für die erste Konstante notwendig. Für die folgenden Konstanten können wir auch auf die Zuweisung verzichten.

const (

Montag = iota // 0

Dienstag = iota // 1

Mittwoch = iota // 2

Donnerstag // 3

Freitag // 4

Samstag // 5

Sonntag // 6

)

// Beginnt hier wieder bei 0

const (

_ = iota // 0 auslassen

eins

_ // 2 auslassen

drei

)

Listing 2–16 Verwendung von iota

Der Zähler zählt immer nur bei einem Aufruf hoch. Das heißt Kommentare oder Leerzeilen werden ignoriert. Wenn wir diesen gezielt um einen Wert hoch setzen möchten, benötigen wir dafür den Blank Identifier _ (Kapitel 2.19).

Die Ausdrücke innerhalb der Deklaration mit iota können auch komplexer sein, wie das folgende Beispiel aus Effective Go zeigt.

type ByteSize float64

const (

_ = iota // ignoriere 0

KB ByteSize = 1 << (10 * iota)

MB

GB

TB

PB

EB

ZB

YB

)

Listing 2–17 Byte-Größen mit iota

Wir verwenden hier den Zähler in Verbindung mit einem Left Shift. Hierüber können wir einfach die korrekte Byte-Größe für KB, MB, GB usw. berechnen.

2.4Pointer

Wie bei anderen Sprachen können in Go Variablen auch Zeiger auf bestimmte Bereiche im Arbeitsspeicher sein. Diese Pointer beinhalten selbst keinen Wert, sondern zeigen nur auf eine andere Variable.

image

Abb. 2–1 Der Gopher zeigt auf eine Adresse des Arbeitsspeichers.

Wenn wir als Typ einen Pointer definieren, dann verwenden wir *. Ein Pointer auf int wird somit als *int geschrieben. Wenn wir von einer bestehenden Variablen die Speicheradresse auslesen, dann setzen wir & vor die Variable.

func main() {

var a int

var b *int // Typ Pointer auf int

a = 123

b = &a // Speicheradresse mit &

fmt.Println(b, *b)

*b = 100 // Dereferenzierung

fmt.Println(a)

}

// Output:

// 0x40e020 123

// 100

Listing 2–18 Funktionsweise des Pointers

In diesem Beispiel wird deutlich, wie einfach ein Pointer zu erzeugen ist. Wenn wir den Wert von b ausgeben, erhalten wir somit auch die Adresse im Arbeitsspeicher. Wenn wir den Wert benötigen oder verändern möchten, müssen wir die Variable dereferenzieren. Dies erfolgt wieder mit dem vorangestellen *. Wenn wir den Wert eines Pointers verändern, ändert sich der Wert im Arbeitsspeicher. In unserem Beispiel ist dies der Speicher der Variable a. Deshalb können wir über den Pointer auch den Wert von a ändern.

In Go ist es ebenfalls möglich, dass eine Funktion nicht den Wert einer Variablen, sondern einen Pointer zurückmeldet.

func foo() *int {

bar := 123

return &bar

}

func main() {

a := foo()

fmt.Println(a, *a)

}

Listing 2–19 Funktion liefert einen Pointer zurück.

Die Variable bar ist innerhalb der Funktion foo() gültig. Normalerweise werden lokale Variablen nach dem Funktionsaufruf innerhalb des Arbeitsspeichers wieder gelöscht. Da wir diesen Bereich des Arbeitsspeichers innerhalb von main() weiter verwenden, bleibt dieser Wert bestehen, solange er benötigt wird.

2.5Eigene Typen

Die durch Go bereitgestellten Basistypen bilden das Grundgerüst. Damit lässt sich schon einiges umsetzen. Wir können natürlich auch eigene Typen definieren und dadurch unseren Programmen weiteren Kontext mitgeben. Schauen wir uns dazu ein einfaches Beispiel an.

func MeterToZentimeter(m int) int{

return m * 100

}

Listing 2–20 Umrechnung von Meter nach Zentimeter

Die Funktion MeterToZentimeter rechnet eine Länge in Meter nach Zentimeter um. Als Datentyp verwendet sie int. Das ist technisch korrekt. Wir könnten hier aber dem Ganzen mehr Kontext geben, wenn wir für Meter und Zentimeter eigene Typen definieren würden.

type meter int

type zentimeter int

Listing 2–21 Definition eigener Typen

Eigene Typen werden dabei mit type definiert, danach folgt der Name des Typs und dann der dazugehörige Basistyp.

Wie schon bei den Variablen ist es möglich, auch Typdefinitionen zu gruppieren.

type (

meter int

zentimeter int

)

Listing 2–22 Gruppierung bei der Definition

Jetzt ändern wir unsere Funktion so, dass diese mit den unterschiedlichen Typen meter und zentimeter funktioniert.

func MeterToZentimeter(m meter) zentimeter {

return zentimeter(m * 100)

}

Listing 2–23 Funktion mit eigenen Typen

Wenn wir jetzt unsere Funktion nutzen wollen, müssen wir als Input immer meter verwenden und erhalten als Output immer zentimeter. Der Compiler ist an dieser Stelle sehr streng. Innerhalb der Return-Anweisung benötigen wir für die Umrechnung eine Typumwandlung, die wir in Kapitel 2.6 ausführlich betrachten werden.

2.6Typumwandlung

Wenn wir in unserem Code Variablen unterschiedlichen Typs verarbeiten, wirft der Compiler einen Fehler. Denn wegen der strengen Typisierung dürfen wir keine Typen mischen. Wir können nicht Äpfel mit Birnen vergleichen. Wenn wir in Go versuchen, Äpfel und Birnen zu addieren, bekommen wir vom Compiler eins auf die Mütze.

func main() {

type äpfel int

type birnen int

a := äpfel(10)

b := birnen(5)

fmt.Println(a + b)

}

Output:

// invalid operation: a + b (mismatched types äpfel and birnen)

Listing 2–24 Äpfel plus Birnen

Ein direkter Vergleich von Äpfeln und Birnen ist also nicht zulässig. Wenn wir aber z. B. nur die Anzahl an Obst berechnen wollen, dann müssen wir unsere Variablen umwandeln. Hierfür verwenden wir die eingebaute Typumwandlung. Da wir sowohl äpfel als auch birnen als Basistyp int verwenden, sollte die Umwandlung problemlos klappen.

Für eine Typumwandlung gibt es zu jedem Typ eine Funktion mit dem Namen des Typs. Das gilt sowohl für Basistypen als auch für eigene Typen.

func main() {

type äpfel int

type birnen int

a := äpfel(10)

b := birnen(5)

anzahl := int(a) + int(b)

fmt.Printf("Anzahl Früchte: %d", anzahl)

}

Listing 2–25 Anzahl an Obst

Für unser Beispiel wandeln wir a und b mit int()