Eine Einführung in die Programmiersprache Go

Tags:

Eines vorne weg: Ich bin in erster Linie PHP-Programmierer. Und ich denke, ein Grossteil meiner Leser auch. Deshalb sind einige Erklärungen in diesem Artikel mit PHP Vergleichen versehen. Aber auch nicht PHP-ler sollten einigermassen gut zurecht kommen. Lasst es mich wissen. :)

Go hat in den letzten Monaten einiges an Aufmerksamkeit erhalten. Persönlich habe hab ich das bisher aber nur aus der Ferne mitverfolgt. Mit dem Erscheinen der Version 1 vor ein paar Tagen nahm ich mir etwas Zeit, um mich in Go reinzufuchsen. Nachfolgend ein paar zusammengetragene Fakten und Meinungen.

Inhalt

Was ist Go?

Fangen wir mal mit einem Wikipedia-Zitat an:

Go ist in erster Linie als Sprache zur Systemprogrammierung gedacht. Ebenso wie die Programmiersprachen C und C++ bietet Go die Möglichkeit, maschinennah zu programmieren. Es wird jedoch versucht, auf fehleranfällige Sprachmittel zu verzichten und diese durch modernere Konzepte zu ersetzen. Weiterhin soll dem zunehmenden verteilten Rechnen auf Mehrkernprozessoren oder Rechnerverbünden Rechnung getragen werden.

Klingt nach einem grossen Unterschied zu PHP, da Go ein ganz anderes Anwendungsgebiet zu haben scheint. Systemprogrammierung? Sowas machen wir Webentwickler doch nicht. Uns interessiert die Speicherverwaltung nicht und um Mehrkernprozessoren wollen wir uns auch nicht kümmern. Das mit den Rechnerverbünden klingt allerdings ganz nett, soll die Wolke doch die Zukunft sein.

Schauen wir uns mal folgenden Code an.

package main

import (
    "fmt"
    "html"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(rsp http.ResponseWriter, req *http.Request) {
        fmt.Fprintf(rsp, "Your IP is: %s", html.EscapeString(req.RemoteAddr))
    })

    http.ListenAndServe(":9000", nil)
}

Diese paar Zeilen reichen aus, um einen eigenen HTTP-Server auf Port 9000 zu starten, der die IP-Adresse des Besuchers zurückgibt. Ziemlich cool, oder? Da Go nötige Bindings für z.B. MySQL mitbringt, könnten wir also problemlos eine standalone REST Hypermedia API schreiben. Go eignet sich also sehr gut für Web-zentrierte Programmierung.

Installation und Workspaces

Zuerst eine kurze Installationsanleitung (für Windows).

Workspaces

Wie bereits angedeutet, zeigt die GOPATH Umgebungsvariable auf euren Workspace. Wenn ihr etwas in Go programmieren wollt, dann solltet ihr das immer in eurem Workspace tun. Ihr könnte auch mehrere Workspaces aufsetzen, doch diese Komplexitätsstufe wollen wir in diesem Artikel nicht erklimmen.

Euer Workspace besteht aus drei Verzeichnissen.

%GOPATH%/
    bin/
    pkg/
    src/

Der Code kommt ins src Unterverzeichnis. Wenn ihr mit go build ein Package kompiliert, wird der Zwischencode im pkg Verzeichnis gespeichert. Wenn ihr mit go install ein Programm baut, wird die kompilierte Binärdatei (.exe unter Windows) im bin Verzeichnis abgelegt. Da dieses Verzeichnis in eurer PATH Umgebungsvariable sein sollte, könnt ihr das Programm danach systemweit direkt aufrufen.

IDE

Ich empfehle euch die GolangIDE/LiteIDE von code.google.com/p/golangide/downloads/list. Sie ist bisher die einzig brauchbare Go IDE, die ich finden konnte. Ausserdem funktioniert sie auf allen Plattformen und bietet Unterstützung für die Eigenheiten von Go.

Was unterscheidet Go?

Go lässt sich ähnlich systemnah wie C, C++ oder Java programmieren, fühlt sich aber dennoch sehr dynamisch und leichtfüssig an. Ein paar Beispiele:

Sprachliches

Kompilierung

Wie bereits erwähnt, ist Go eine kompilierte Sprache. Und dabei geht der Compiler sehr streng vor. So bricht die Kompilierung z.B. bereits mit einem Fehler ab, wenn ihr eine Variable oder ein importiertes Package nicht genutzt habt.

Ein sehr nettes Feature von Go ist der run Modus. Damit kann man ein Go Programm wie durch einen Interpreter ausführen lassen. Im Hintergrund kompiliert Go einfach den Code in einem temporären Verzeichnis und startet anschliessend das Programm.

cd %GOPATH%\src\myapp
go run main.go

... ist das Selbe wie ...

cd %GOPATH%\src\myapp
go build
myapp.exe

Mit dem Aufruf von go install könnt ihr Packages und Binaries sogar systemweit installieren und somit wiederverwenden.

go install %GOPATH%\src\myapp
myapp.exe

Obiger Aufruf kompiliert das myapp Programm und kopiert die myapp.exe ins Verzeichnis %GOPATH%\bin, welches in der eigenen PATH Umgebungsvariable sein sollte.

Packages

Code ist in Go in Packages eingeteilt, die per import, nun, importiert werden. Das spezielle main Package wird stets in eine ausführbare Datei kompiliert, alle anderen Packages werden kompiliert und können dann von Programmen per import genutzt werden.

Achtung: Packages sind nicht Namespaces. Der Namensraum wird durch die Verzeichnisstruktur gegeben. Der Name des Package entspricht dem letzten Teil des Namespaces. Ein Beispiel:

%GOPATH%/
    src/
        namespace/
            morenamespace/
                package1/
                    somefile.go        // Package "package1"
                    someotherfile.go   // Package "package1"
                    subpackage1/       // "package1" ist hier Teil des Namensraums für "subpackage1"
                        file.go        // Package "subpackage1"
                package2/
                    yetanotherone.go   // Package "package2"

Code, der diese Packages verwendet, würde dann so aussehen.

package main

import (
    "namespace/morenamespace/package1"
    "namespace/morenamespace/package1/subpackage1"
    "namespace/morenamespace/package2"
)

func main() {
    a := package1.SomeFunc()
    b := subpackage1.Constant
    c := package2.Type{}
}

Falls der Fall auftreten sollte, dass zwei Packages den selben Namen exportieren, kann im ìmport Statement gesagt werden, welches Package man wie ansprechen möchte.

// Fehler: "math redeclared as imported package name"
import (
    "math"                       // "math" Package von Go
    "github.com/bigwhoop/math"   // Mein eigenes "math" Package
)

// Importiert "coremath" und "mymath"
import (
    coremath "math"
    mymath   "github.com/bigwhoop/math"
)

// Importiert "math" und "mymath"
import (
    "math"
    mymath "github.com/bigwhoop/math"
)

Semikolone und Klammern

Wie ihr am Code oben erkennen könnt, braucht es in Go keine Semikolone. Ihr könnte sie zwar optional setzen, doch dank seiner einfachen Grammatik kann dies Go selbständig viel schneller als ihr. Göhnt der ,-Taste etwas Ruhe. :)

Geschweifte Klammern bei Funktionen, Kontrollstrukturen, etc. müssen immer am Ende der Zeile beginnen und können nicht weggelassen werden (es gibt also kein Dangling Else Problem).

Variablendeklaration

Variablen werden immer initialisiert. D.h., wenn ihr keine Initialisierungswert angebt, wird der Standardwert des Datentyps genommen; die sogenannte Zero Value. Dieser Wert ist je nach Datentyp unterschiedlich.

var b bool        // var b bool      = false
var i int         // var i int       = 0
var p *int        // var p *int      = nil
var s string      // var s string    = ""
var f float64     // var f float64   = 0
var c complex64   // var c complex64 = (0+0i)

var x struct{ i int, s string }
// var x struct{ i int; s string } = struct{ i int; s string }{ i: 0, s: "" }

Der Datentyp kommt immer nach dem Variablennamen. Das hat den Vorteil, dass man Variablen mit gleichem Datentyp auf einer Zeile deklarieren kann. Das funktioniert auch mit Pointer (im Gegensatz zu C).

var i, j, k int
// var i int = 0
// var j int = 0
// var k int = 0

Falls ihr Initialisierungswerte übergebt, muss der Typ nicht angegeben werden. Es wird der vom Initilialisierungswert übernommen

var b, i, s = false, 5, "hallo"
// var b bool = false
// var i int = 5
// var s string = "hallo"

Mit Hilfe des := Operators kann man sich das var sparen. Das funktioniert aber nur innerhalb von Funktionen.

i, s := 5, "hallo"
// var i int = 5
// var s string = "hallo"

Pointers

Go unterstützt Pointers. Also den Zugriff auf die Speicheradresse, wo Daten einer Variable gespeichert sind. Dazu gibt es den & Operator.

i := 5
//  i = 5
// &i = 0xf8400013e0

Um einen Pointer zu dereferenzieren (also die Referenz auf seinen Wert aufzulösen), nutzt man den * Operator.

i := 5
// var i int = 5
//     i = 5
//    &i = 0xf8400013e0
//   *&i = 5

p := &i
// var p *int = &i
//     p = 0xf8400013e0
//    *p = 5
//   &*p = 0xf8400013e0

Bevor ihr jetzt davon rennt: Ihr könnt ihn Go nicht direkt auf das Memory zugreifen, es gibt keine Pointer Arithmetik. Pointers werden in Go gebraucht, um Variablen per Referenz zu übergeben. Also etwa genauso wie in PHP Objekte per Referenz übergeben werden. In Go ist standardmässig alles pass-by-value. D.h. bei jedem Funktionsaufruf, jedem Assignment, etc. wird kopiert. Sogar ganze Objekte. Um dies zu verhindern, empfiehlt es sich, anstatt des ganzen Objektes nur den Pointer (also die Referenz auf das eigentliche Objekt) zu übergeben. Der Pointer wird zwar auch kopiert, aber das ist nicht so tragisch. Es ist wie wenn ihr die URL zu diesem Artikel shared, anstatt den gesamten Text.

Pointers zu Objekten müsst ihr in Go nicht dereferenzieren. Go kann das selbständig tun und ihr könnt Methoden mit der selben . Syntax aufrufen, wie ihr das auch bei "Values Types" tut.

type Foo struct {
    x int
}

// Methode "SetX" für Values
func (this Foo) SetX(x int) {
    this.x = x
}

// Methode "SetX" für Pointers
func (this *Foo) SetX(x int) {
    this.x = x    // <- this.x muss nicht speziell aufgerufen werden
}

a := Foo{}    // Foo object
a.SetX(5)

b := &Foo{}   // Pointer of Foo object
b.SetX(5)     // <- SetX() muss nicht speziell aufgerufen werden

Dieses Beispiel greift jetzt etwas vorweg ... lest einfach weiter. :)

Kontrollstrukturen

Das if unterscheidet sich eigentlich nur in einem Punkt von üblichen if's: Es braucht keine Klammern um die Condition, dafür immer Klammern um den Body. Ausserdem kann das if ein Initialisierung-Statement besitzen.

// Normal
if 5 < 10 {
    // ...
}

// Mit Initialisierung-Statement
if i := 5; i < 10 {
    // ...
}

Anstatt for, foreach, while und do ... while gibt es in Go nur for. Dieses kann aber die ersten drei Kontrollstrukuren abdecken:

// for
for i := 0; i < 10; i++ {
    // ...
}

// while
for i < 10 {
    // ...
}

// foreach
arr := [3]int{1,2,3}
for key, value := range arr {
    // ...
}

// foreach nur mit Key
for key := range arr {
    // ...
}

// foreach nur mit Value
for _, value := range arr {
    // ...
}

Fehlerbehandlung

In Go gibt es keine Exceptions, sondern Fehler Objekte. Diese werden von Funktionen zurückgegeben. Das heisst der Caller muss sich stets um den Fehler kümmern. Da Go - wie ihr später genauer erfährt - meherere Rückgabewerte unterstützt, sieht man oft solche Konstrukte...

c, err := ReadFile("path/to/file.txt")
if err != nil {
    // Fehler behandeln
}

Die Funktion ReadFile() gibt als zweiten Wert also entweder nil oder ein Fehler Objekt zurück. Fehler sollten stehts vom Typ error (ein Interface) sein. Dazu müssen sie lediglich eine Methode Error besitzen, die einen String zurückgibt. Wie das genau funktioniert, sollte am Ende des Artikels klar sein.

Stellt ein Programm einen nicht behebbaren Fehler fest (z.B. eine fehlende Library), darf es die panic() Funktion aufrufen. Diese Funktion ist aber nicht wie PHPs exit() und bricht mehr oder weniger sofort ab. Sie geht stattdessen rückwärts durch den ganzen Call Stack und führt alle "verzögerten Funktionen" (was das ist, erfahrt ihr später) aus und kann mit recover() "abgefangen" werden (aber nur innerhalb dieser speziellen Funktionen). Erreicht Go den Start des Call Stacks, dann bricht es das Programm ab.

Datentypen / Objekt-Modell

Keine Klassen

Anstatt Klassen gibt es Types und Funktionen. Die Funktionen lassen sich zu Methoden erheben, in dem man sie an einen Type bindet. Types und Methoden sind grundsätzlich aber zwei separate Konzepte. Sie sind nicht das Pendant zu Klassen(instanzen) und Methoden der gängigen Objekt-orientierten Programmierung. Wieso versteht ihr hoffentlich gleich etwas besser.

Es gibt drei Arten von Types:

Basic Types

Go kennt die folgenden Basic Types. Stellt sie euch wie die skalaren Datentypen in PHP vor.

Go erlaubt es euch einfach einen eigenen Type "basierend" (keine Veerbung) auf einem dieser Types zu definieren.

type myint int

Strukturen

Strukturen könnt ihr euch wie stdClass Objekte vorstellen. Also Objekte mit Feldern (fields).

type Person struct {
    Firstname string
    Lastname  string
}

Objekte von Strukturen lassen sich auf folgende Arten erstellen.

// Ohne Initialisierungswerte
p := Person{}   // { Firstname: "", Lastname: "" }

// In der Reihenfolge wie die Struktur definiert wurde
p := Person{"Philippe", "Gerber"}   // { Firstname: "Philippe", Lastname: "Gerber" }
p := Person{"Gerber", "Philippe"}   // { Firstname: "Gerber", Lastname: "Philippe" }

// Mit parametrisierten Werten
p := Person{Firstname: "Philippe", Lastname: "Gerber"}   // { Firstname: "Philippe", Lastname: "Gerber" }
p := Person{Firstname: "Philippe"}                       // { Firstname: "Philippe", Lastname: "" }
p := Person{Lastname: "Gerber"}                          // { Firstname: "", Lastname: "Gerber" }

Natürlich könnt ihr auf die Felder einer Struktur auch zugreifen. Allerdings nur wenn sie exportiert werden (dazu gleich mehr).

p := Person{"Philippe"}
f := p.Firstname   // "Philippe"
l := p.Lastname    // ""

Methoden

An Typen könnt ihr Methoden "anhängen", indem ihr sie bei der Definition einer Funktion als Empfänger (receiver) angebt.

func (this Person) GetFullname(format string) string {
    return fmt.Sprintf(format, this.Firstname, this.Lastname)
}

Dabei ist wichtig zu unterscheiden, ob der Empfänger vom Typ Pointer eines Datentyps oder vom Typ Datentyp ist.

type TwoNumbers struct {
    a, b int
}

// ---

// Methode für Objekte vom Type "TwoNumbers"
func (this TwoNumbers) Add() int {
    return this.a + this.b
}

n1 := TwoNumbers{5, 10}    // Type "TwoNumbers"    
n1.Add()                   // 15
n1.Multiply()              // Fehler: Methode gibt es nicht

// ---

// Methode für Objekte vom Type "*TwoNumbers" oder "Pointer of TwoNumbers"
func (this *TwoNumbers) Multiply() int {
    return this.a * this.b
}

n2 := &TwoNumbers{20, 3}   // Type "*TwoNumbers" oder "Pointer of TwoNumbers"    
n2.Add()                   // Fehler: Methode gibt es nicht
n2.Multiply()              // 60

Achtung: Wenn ihr Daten im Objekt verändert wollt, müsst ihr den Receiver der Methode stets als Pointer definieren. Ansonsten macht ihr die Änderung nur an einer Kopie. Für die Empfänger gelten nämlich die selben Regeln wie für Funktionsargumente: Parmeter werden kopiert.

Funktionen

Hat eine Methode keinen Empfänger ist sie eine gewöhnliche Funktion.

func NewPerson(firstname, lastname string) Person {
    return Person{firstname, lastname}
}

Das obige Beispiel zeigt übrigens wie man einen Konstruktur in Go implementiert. Man schreibt sich einfach eine kleine Hilfsfunktion. Und wenn das Package persons heisst, kann muss die Funktion auch nur New heissen und schon haben wir mit persons.New() eine hübsche Schreibweise.

Funktionen mit mehreren Rückgabewerten

Funktionen und Methoden können in Go beliebig viele Rückgabewerte besitzen. Bei der Zuweisung müssen diese Rückgabewerte vom Caller auch alle behandelt werden.

func OneNumber() int {
    return 5
}
n := OneNumber()
// var int n = 5

func TwoNumbers() (int, int) {
    return 23, 65
}
n1, n2 := TwoNumbers()
// var n1 int = 23
// var n2 int = 65

Da Go die Kompilierung mit einem Fehler abbricht, wenn eine Variable ungenutzt ist, gibt es das spezielle Unterstrich Konstrukt. Nutzt man anstatt einer Variable den _, kann die Deklaration ignoriert werden. Bzw. man kann dann auch gar nicht mehr auf den Rückgabewert zugreifen.

_, n1 := TwoNumbers()
// var n1 int = 65

Verzögerte Funktionsaufrufe

Go kennt mit dem defer ein spezielles Schlüsswort, dass euch anonyme Funktionen innerhalb von Methoden ausführen lässt, direkt bevor die Methode verlassen wird. Damit könnt ihr zum Beispiel offene Dateiressourcen schliessen oder eine temporäre Datei löschen.

func Contents(filename string) (string, error) {
    f, err := os.Open(filename)
    if err != nil {
        return "", err
    }

    defer f.Close()

    // Datei auslesen ...
}

In diesem Beispiel wird f.Close() auf jeden Fall beim Verlassen der Contents() Funktion aufgerufen werden, egal was nach dem Aufruf von defer noch geschehen mag. Der Vorteil davon ist, dass ihr a) die Dateiressource auf jeden Fall schliessen werdet und b) die Schliessanweisung nahe bei der Öffnungsanweisung steht - und nicht erst 100 Zeilen später.

Interfaces

Interfaces sind eine grosse Stärke von Go. Sie werden nicht wie in PHP oder Java von Klassen implementiert, sondern eher wie beim Duck Typing automatisch an die Methoden zugewiesen, die die selbe Methodensignatur wie das Interface aufweisen.

type Reader interface {
    Read(b []byte) (n int, err error)
}

type Writer interface {
    Write(b []byte) (n int, err error)
}

// Interface das zwei andere Interfaces nutzt
type ReadWriter interface {
    Reader
    Writer
}

Was ihr hier seht sind ein paar Standard-Interfaces die Go mitbringt. Wenn wir jetzt einen eigenen Type erstellen würden, der eine Methode mit der Signatur Write(b []byte) (n int, err error) hätte, dann implementierte dieser Type automatisch das Writer Interface.

type MyFile struct {
    Path string
}

func (this *MyFile) Write(b []byte) (n int, err error) {
    // Logik um in Datei zu schreiben...
}

Der eigentliche Nutzen macht sich erst dann bemerkbar, wenn man Interfaces als Type Hints in seinen Funktionen nutzt, bzw. als Datentyp seiner Variablen.

func SayHello(w Writer) {
    s := "Hello"
    w.Write([]byte(s))
}

var w Writer

w = MyFile       // meine Funktion
SayHello(w)

w = os.Stream    // eine Go Funktion
SayHello(w)

Sichtbarkeiten

In Go sind die Sichtbarkeiten von Typen, Methoden, Konstanten, Variablen, etc. per Package geregelt und es gib keine public, private und protected Schlüsselwörter. Es gibt nur private und öffentliche Sprachelemente, und die Regel dazu ist ganz simpel:

Ein Beispiel. Stellt ich folgende Datei somepackage/file.go vor.

package somepackage

type MyType1 struct {
    Field1, field2 string
}

func create1() *MyType1() {
    return &MyType1{}
}

type myType2 struct {
    Field1, field2 string
}

func Create2() *myType2() {
    return &myType2{}
}

Un dann eine Datei main.go.

package main

import "somepackage"

func main() {
    t := somepackage.MyType1{}   // Ok
    t := somepackage.create1{}   // Fehler: Funktion "create1" wird nicht exportiert
    t := somepackage.myType2{}   // Fehler: Struktur "myType2" wird nicht exportiert
    t := somepackage.Create2{}   // Ok
}

Jetzt seht ihr auch wie man Konstruktoren implementieren kann: Man macht den Type private und bietet eine public Funktion an, die eine Instanz des Types zurückgibt.

Arrays und Slices

In PHP sind Arrays eine All-in-one Lösung. In (allen) anderen Sprachen ist ein Array nur eine Reihe von Elementen. Deshalb haben Arrays dort normalerweise nur numerische Indexe und eine fixe Grösse.

In Go ist das nicht anders. Arrays gibt es für alle Typen, in dem man diesen ein [<anzahl elemente>] voranstellt.

var a [2]int = {5, 3}      // [2]int{5, 3}
var b [3]int = {1, 2, 3}   // [3]int{1, 2, 3}

Wichtig hierbei ist, dass [2]int und [3]int zwei unterschiedliche Datentypen sind. Man kann die also nicht mixen. Auch kann ein Array nicht mehr erweitert werden. Aus einem [3]int Array lässt sich also kein [4]int Array machen, indem man ein weiteres Element anhängt. Man muss dazu eine neues Array erstellen.

Auch bei Arrays gilt wieder das Konzept mit den zero values.

var c [4]int      // [4]int{0, 0, 0, 0}
var d [2]string   // [2]string{"", ""}

Mit Hilfe von [...] kann man Go die Grösse des Arrays selber zählen lassen.

var e [...]string = {"hallo", "welt!"}   // [2]string{"hallo", "welt!"}

Das klingt ja jetzt alles etwas unflexibel. Und damit habt ihr absolut Recht. Abhilfe schafft Go mit sogenannten Slices. Deren Notation ist sehr ähnlich wie die der Arrays.

var f1 []int = {1, 2}  // []int{1, 2}
var f2 []int           // []int nil
f3 := {1, 2, 3}        // []int{1, 2, 3}

Slices basieren auf Arrays, haben aber keine feste Grösse. Ihr könnt euch einen Slice wie einen Paragraphen eines Textes vorstellen, wobei der Text ein Array von Paragraphen wäre. Ein Slice ist ein Pointer auf ein Element des Array, plus einer Grössenangabe. Genauere Informationen zur internen Abbildung von Sices findet ihr in diesem Artikel.

Mit Hilfe von [<Index Start>:<Index Ende>] könnt ihr Slices zerhacken.

h := []string{"p", "h", "i", "l", "i", "p", "p", "e"}
i := h[:3]    // []string{"p", "h", "i", "l"}
j := h[2:]    // []string{"l", "i", "p", "p", "e"}
k := h[2:5]   // []string{"l", "i", "p"}
l := h[:]     // []string{"p", "h", "i", "l", "i", "p", "p", "e"}

Um einen Slice aus einem Array zu erstellen, könnt ihr ebenfalls [:] nutzen.

m := [5]string{"h", "a", "l", "l", "o"}    // Array [5]string{"h", "a", "l", "l", "o"}
n := m[:]                                  // Slice  []string{"h", "a", "l", "l", "o"}

Go bietet ein paar Funktionen für den einfachen Umgang mit Slices:

// make([]T, len, cap) []T - Erstellt einen Slices
o := make([]int, 3)    // Slice []int{0, 0, 0} mit unterliegendem Array [3]int{0, 0, 0}
p := make([]int, 2, 4) // Slice []int{0, 0} mit unterliegendem Array [4]int{0, 0, 0, 0}

// copy(dst, src []T) int - Kopiert einen Slice in einen anderen
dst := []int{1, 2, 3}
src := []int{5, 6}
copy(dst, src)   // dst = []int{5, 6, 3}

// append(s []T, x ...T) []T - Fügt Elemente zu einem Slice hinzu
q := []int{1, 2, 3}        // []int{1, 2, 3}
r := []int{5, 6}           // []int{5, 6}
q = append(q, r...}        // []int{1, 2, 3, 5, 6}
q = append(q, 7, 8, 9}     // []int{1, 2, 3, 5, 6, 7, 8, 9}
q = append(q, r[0], r[1}   // []int{1, 2, 3, 5, 6, 7, 8, 9, 5, 6}

// len([]T) int - Gibt die Länge (=Anzahl Elemente) eines Slices
//                (und Arrays, Strings, etc.) zurück
r := []int{1, 2, 3}   // []int{1, 2, 3}
s := len(r)           // 3 int

// cap([]T) int - Gibt die Anzahl Elemente des Arrays (!) an, die
//                nach dem vom Slice referenzierten Element noch folgen
t := []int{1, 2, 3, 4, 5}   // []int{1, 2, 3, 4, 5}
u := t[1:3]                 // []int{2, 3}
v := cap(u)                 // 4 int

Maps

Maps sind Dictionaries. Oder in PHP-Sprache: Assoziative Arrays, wo aber sowohl die Keys, als auch die Values, einen festen Typ haben.

m := map[string] bool {
    "Tom"   : true,
    "James" : false,
    "Mark"  : false,
    "Jack"  : true
}

Zugriffe auf Elemente einer Map funktionieren per [<key>].

m := map[int] string {
    20 : "Hallo",
    42 : "Welt"
}

a := m[20]   // "Hallo" string
b := m[42]   // "Welt" string

Ist ein Element nicht vorhanden, wird die zero value des Datentyps zurückgegeben.

m := map[string] string {
    "vorhanden" : "ja"
}

a := m["nichtvorhanden"]   // "" string

Um zu überprüfen ob eine Wert in einer Map ist, kann man den zweiten Rückgabewert überprüfen. Dieser ist true wenn ein Element gefunden wurde, ansonsten false.

m := map[string] string {
    "vorhanden" : "ja"
}

a, b := m["vorhanden"]
// var a string = "ja"
// var b bool = true

_, c := m["nichtvorhanden"]
// var c bool = false

Um ein Element zu löschen, nutzt man die delete() Funktion.

m := map[string] bool {
    "12" : false
}

delete(m, "12")

Unicode, Strings und Byte-Slices

In PHP kennen wir den Datentyp string. Dabei handelt es sich eigentlich nur um ein Array von Bytes. Das kann man einfach beweisen, in dem man Sonderzeichen in seine Strings einfügt.

<?php
echo strlen('Laute'); // 5
echo strlen('Hütte'); // 6

Da das ü zwei Bytes benötigt und strlen einfach nur die Bytes zählt, erhalten wir für die zweite Zeile ein falsches Ergebnis. Abhilfe schafft die Multibyte-Extension mit der Funktion mb_strlen().

Bei Go hingegen ging nicht vergessen, dass es nebst dem Englisch noch weitere Sprachen mit anderen Zeichen gibt. Deshalb ist Go voll auf Unicode ausgelegt. Und deshalb gibt es auch den Unterschied zwischen Strings und Byte-Slices ([]byte).

Go bietet Packages an, die dem Entwickler bei der Konvertierung von Strings zu []bytes (und andersrum) helfen. Als PHP-Programmierer musste ich mich aber erst mal daran gewöhnen, dass manche Funktionen Strings und andere Byte-Slices benötigen, bzw. zurückgeben. Aber das ist alles halb so wild, keine Angst.

Weitere Goodies

Eigener Code Formatierer

Go bringt ein Tool mit, um seinen Code zu formatieren. Einfach go fmt <script> ausführen und die Frisur sitzt. Einigen mag das jetzt sicherlich sauer aufstossen, da Style Guides doch jeder/jedes Team selbständig erarbeiten sollte. Doch seht es so: Wenn eh alle die Semantiken einer neuen Programmiersprache lernen müssen, dann kann es nur hilfreich sein, wenn jeglicher Code auf der ganzen Welt von Anfang an gleich ausschaut.

Anbindung an GitHub und Google Code

Go kann Packages direkt von GitHub und Google Code runterladen. Dazu muss man in einem Programm als import-Statement nur github.com/username/repository schreiben, und Go kann danach selbständig mit go get das Package runterladen und installieren/kompilieren.

Eingebautes Testing Framework

Go kommt direkt mit eigenem Unit Testing Framework. Dazu muss man seine *.go Datei nur in *_test.go enden lassen, im Code das testing Package importieren und Methoden mit der Signatur Test<NameDesTests>(t *testing.T) hinzufügen.

// Datei "min_max_test.go"
package core

import (
    "testing"
)

func TestGreaterThan(t *testing.T) {
    if 1 > 10 {
        t.Errorf("Oh noes, 1 is bigger than 10?!?!!")
    }
}

Fazit

Ich hab mich ein bisschen verliebt, in den letzten Wochen. Go wirkt wie ein aufgeräumtes Zimmer: Übersichtlich, sauber, frei von Ablenkung. Die Türe ist einfach zu öffnen, es steht kein Gerümpel im Weg. Als Konsequenz hat man die ganze Arbeitsfläche zur Verfügung. Die ganzen Tools sind sofort verfügbar. Vielleicht findet man das eint oder andere Werkzeug auf die Schnelle nicht, weil man es zu gut verstaut hat. Oder weil man den zum Hammer umfunktionierten Stein entgültig entsorgte. Mit einer kleinen Suche lässt sich aber stets schnell eine gute Lösung und ein Ersatz finden.

Genug mit schlechten Analogien. Guckt euch Go an. Es wirkt extrem durchdacht. An manchen Ecken noch etwas grob, aber das sollte kein Grund sein, nicht jetzt einzusteigen und mit dem Wachstum der Sprache zu lernen. Ich sehe definitiv eine Zukunft.

Ähnliche Artikel

Kommentare