Mein erstes Go-Programm

6 minute read Published:

Ich habe die letzten Tage genutzt mich in die Programmiersprache Go einzuarbeiten.

Nachdem ich vor allem Beispiele abgetippt und mir Fremdcode angeschaut habe, schrieb ich heute mein erstes eigenes Programm. Wenig innovativ handelt es sich um eine Variante von Hello World, wobei ich versucht habe, einige erweiterte Techniken anzuwenden.

Das Programm ist in der Lage abhängig vom Vorhandensein von Kommandozeilenparametern eine von zwei Greeter-Implementierungen zu wählen, die jeweils einen String zurückgeben. Dieser wird dann gefolgt von einem Zeilenumbruch ausgegeben.

Installation von Go

Ich habe zuerst Go 1.10 aus den Paket-Repositories von Archlinux installiert und in der Datei $HOME/.bashrc die Variable $GOPATH auf die Wurzel meiner Go-Repositories gesetzt.

Das erste Programm

Dann habe ich in diesem Verzeichnis die Struktur für mein Programm angelegt:

mkdir -p softmetz.de/hello/greeter

Das Hauptprogramm

In softmetz.de/hello wurde die Datei main.go angelegt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import (
        "softmetz.de/hello/greeter"
        "fmt"
)

func main() {

        g := greeter.NewGreeter()
        greeting := g.Greeting()

        fmt.Println(greeting)

}

Das ist die Einstiegsdatei für das Go-Programm. In Go ist es Pflicht, dass diese Dateien im Package main sind.

Der import-Block enthält zwei Einträge. Während fmt aus der Standardbibliothek Funktionen zur Formatierung und Ausgabe von Text bereitstellen, ist softmetz.de/hello/greeter eine Bibliothek, die zum Programm gehört.

Die Funktion main ist dann relativ übersichtlich. Zuerst wird eine Instanz vom Interface Greeter erzeugt. greeter ist dabei das Package der Bibliothek und NewGreeter ist ein Funktion, die die Rolle einer Factory innehält.

Im nächsten Schritt wird die Methode Greeting aufgerufen, welche die Begrüßung als String zurückliefert.

Zuletzt wird die Begrüßung gefolgt vom Umbruch auf der Standardausgabe gedruckt.

Die Bibliothek greeter

Die Bibliothek besteht aus zwei Dateien. Einmal der eigentlichen Implementierung und dann noch einem Test.

Zuerst die Datei greeter.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package greeter

import (
        "log"
        "os"
        "strings"
)

type Greeter interface {
        Greeting() string
}

type DefaultGreeter struct{}

func (g *DefaultGreeter) Greeting() string {
        return "Hello World!"
}

type OsArgsGreeter struct{}

func (g *OsArgsGreeter) Greeting() string {
        params := strings.Join(os.Args[1:], " ")
        return "Hello " + params + "!"
}

func NewGreeter() Greeter {

        switch {
        case len(os.Args) > 1:
                log.Printf("Creating OsArgsGreeter")
                return &OsArgsGreeter{}
        default:
                log.Printf("Creating DefaultGreeter")
                return &DefaultGreeter{}
        }

}

Diesmal entspricht der Package-Name greeter dem Namen der Bibliothek. Es werden die Bibliotheken log, os und strings aus der Standard-Bibliothek von Go importiert.

In Zeile 9 wird das Interface Greeter erstellt. Dieses hat eine Methode Greeting, welche einen String liefert. Go spricht selbst davon, dass es gleichzeitig eine objektorientierte Programmiersprache ist und auch wieder nicht. Das wird unter anderem daran deutlich, dass es möglich ist, mit Interface Polymorphismus zu realisieren, aber das Konzept von Klassen und Vererbung in Go nicht existiert. Stattdessen wird auf Aggregation und Komposition gesetzt.

In den Zeilen 13 bis 17 wird DefaultGreeter implementiert. Auffällig ist, dass nirgends auf Greeter verwiesen wird. In Go wird ein Interface von allen Typen implementiert, die dem Interface entsprechen, also dessen Funktionen mit den gleichen Parametern und Return-Werten enthält. Damit ist der DefaultGreeter geeignet, die Rolle eines Greeter zu vertreten. In diesem Zusammenhang ist noch wichtig, dass Methoden zu Typen nicht innerhalb der geschweiften Klammern der Typ-Defintion implementiert werden, sondern ausserhalb. Das ist so ähnlich wie in C++, wo die Klasse im Header definiert wird und die Methoden dann in der C++-Datei implementiert werden. Das Konzept von this sowie die Verknüpfung zur klasse wird über die erste Klammer in Zeile 15 gesteuert. In diesem Fall ist der DefaultGreeter als g in der Methode erreichbar.

Die zweite Implementierung von Greeter ist OsArgsGreeter. Diese hängt an ein freundliches “Hello” alle Parameter gefolgt von einem Ausrufzeichen an. Die Kommandozeilenparameter bekommt man in Go über das Array/Slice os.Args. Hier ist zu beachten, dass der erste Parameter der Name des Programms ist, also in Etwa wie bei Bash, wo ja $0 auch der Name des Skripts ist und der erste Parameter $1.

Ab Zeile 26 kommt dann die Factory-Methode, die auch in main.go schon bemüht wurde. Darin steckt ein Switch-Konstrukt, welches die Bedingungen in case-Blöcken prüft. Trifft keine Bedingung zu, wird der default-Block ausgeführt. Bei Go muss man keine break-Anweisungen verwenden, das Konzept des “Durchfallens” existiert nicht.

Testen muss sein

Last but not least braucht ein komplexes Programm natürlich auch Testfälle. Go bringt mit go test und der testing-Bibliothek ein eigenes Testframework mit.

Der Code liegt in einer Datei, die *_test.go heisst, in diesem Fall greeter_test.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package greeter_test

import (
        "os"
        "softmetz.de/hello/greeter"
        "testing"
)

func TestDefaultGreeter(t *testing.T) {

        expected := "Hello World!"
        os.Args = []string{}

        g := greeter.NewGreeter()
        actual := g.Greeting()

        if actual != expected {
                t.Errorf("Test failed, expected: %s, got: %s", expected, actual)
        }

}

func TestOsArgsGreeter(t *testing.T) {

        expected := "Hello foo bar!"
        os.Args = []string{"", "foo", "bar"}

        g := greeter.NewGreeter()
        actual := g.Greeting()

        if actual != expected {
                t.Errorf("Test failed, expected: %s, got: %s", expected, actual)
        }

}

Das Package lautet diesmal greeter_test. Das ist keine Regel, aber ich habe es so aus den Testfällen der Go-Standardbibiliothek übernommen.

Der Import-Block ab Zeile 3 importiert wieder os und die greeter-Bibliothken. Außerdem wird das Test-Framework testing eingebunden.

Testfälle liegen in Methoden, die Test...(t *testing.T) heissen. Ganz wichtig ist, dass in diesem Framework nur Fehlersituationen betrachtet werden, es gibt kein Assert` oder ähnliche Konzepte.

Die Tests sollten relativ selbsterklärend sein. Hervorzuheben sind lediglich die Zeilen 12 und 26 verdienen Erklärung. Wie oben schon beschrieben, schreibt Go in das Array os.Args alle Kommandozeienparameter. Da es sich um ein normales Array handelt und nicht etwa um eine Methode, kann es von überall überschrieben werden. Für den Test ist es super, aber ich stelle mir gerade vor was passiert, wenn eine Bibliothek die Parameter modifiziert und damit z.B. Sicherheitsfeatures abschaltet.

Fazit

Go ist eine sehr interessante Sprache und ich plane damit noch mehr zu machen als dieses kleine Programm. Insbesondere interessiert es mich, inwieweit sich Go für Webanwendungen eignet, die sich einfach selbst hosten lassen um damit zu helfen, Silos aufzubrechen.

Das Testframework ist auf den ersten Blick etwas rudimentär, z.B. finde ich t.Errorf("Test failed, expected: %s, got: %s", expected, actual) sehr redundant, bei JUnit gibt es da mehr out of the box. Es gibt wohl auch schon erste Aufsätze auf das Basis-Testing. Mal schauen.

Ansonsten lese ich gerade noch Go at Google: Language Design in the Service of Software Engineering worin die Designentscheidungen dargelegt werden.