2.2 Grundlagen von Go

In diesem Abschnit werden wir lernen, wie man Konstanten und Variablen mit grundlegenden Datentypen definiert, sowie ein paar weitere Fähigkeiten aus der Go-Programmierung.

Variablen definieren

Es gibt syntaktisch viele Wege, eine Variable in Go zu definieren.

Das Schlüsselwort var ist die simpelste Form, eine Variable zu erstellen. Bedenke, dass in Go der Datentyp hinter dem Variablennamen steht.

// Definiere eine Variable mit dem Namen “variableName” vom Typ "type"
var variableName type

Definiere mehrere Variablen.

// Definiere drei Variablen vom Typ "type"
var vname1, vname2, vname3 type

Definiere eine Variable mit einem Startwert.

// Definiere eine Variable mit dem Namen “variableName” vom Typ "type" und dem Wert "value"
var variableName type = value

Definiere mehrere Variablen mit einem Startwert.

/*
Definiere drei Variablen vom Typ "type" und gib ihnen drei Startwerte.
vname1 ist gleich v1, vname2 ist gleich  v2, vname3 ist gleich v3
*/
var vname1, vname2, vname3 type = v1, v2, v3

Findest Du es nicht auch ein wenig mühsehlig, Variablen auf diesem Weg zu definieren? Keine Sorge, denn das Team hinter Go hat auch so gedacht. Daher kannst Du Variablen Startwerte geben, ohne explizit den Datentyp zu definieren. Der Code würde dann so aussehen:

/*
Definiere drei Variablen vom Typ "type" und gib ihnen drei Ausgangswerte.
vname1 ist gleich v1, vname2 ist gleich  v2, vname3 ist gleich v3
*/
var vname1, vname2, vname3 = v1, v2, v3

Schön, ich weiß das dies immer noch nicht einfach genug für Dich ist. Mal schauen, wie wir das lösen können.

/*
Definiere drei Variablen ohne den Typ "type", ohne das Schlüsselwort "var" und gib ihnen Startwerte.
vname1 ist gleich v1,vname2 ist gleich v2,vname3 ist gleich v3
*/
vname1, vname2, vname3 := v1, v2, v3

So sieht es schon viel besser aus. Nutze :=, um var und type zu ersetzen. Es handelt sich hierbei um eine Kurzschreibweise. Aber warte, es gibt einen kleinen Haken: diese Form der Definition kann nur innerhalb von Fuktionen genutzt werden. Der Compiler wird sonst eine Fehlermeldung ausgeben, wenn Du es trotzdem außerhalb der {} einer Funktion versuchst. Daher nutzen wir meist das Schlüsselwort var um globale Variablen zu definieren oder die Funktion var().

_ (Unterstrich) ist ein besonderer Variablenname. Jeder übergebene Wert wird ignoriert. Übergeben wir zum Beispiel 35 an b, so verwerfen wir 34. ( Dieses Beispiel soll nur die Funktionsweise aufzeigen. Es mag, wie in diesem Fall, nutzlos erscheinen, aber wir werden es oft gebrauchen, wenn wir Rückgabewerte von Funktionen erhalten. )

_, b := 34, 35

Wenn Du Variablen in Deinem Programm definierst, aber keine Verwendung finden, wird der Compiler eine Fehlermeldung ausgeben. Versuche den unten stehenden Code zu kompilieren und schaue, was passiert.

package main

func main() {
    var i int
}

Konstanten

Konstanten sind Werte, die während der Kompilierung festgelegt werden und während der Laufzeit nicht veränderbar sind. In Go kannst Du Konstanten als Wert Nummern, Booleans oder Strings geben.

Definiere eine Konstante wie folgt.

const constantName = value
// Du kannst auch den Datentyp hinzufügen, wenn nötig
const Pi float32 = 3.1415926

Mehr Beispiele.

const Pi = 3.1415926
const i = 10000
const MaxThread = 10
const prefix = "astaxie_"

Grundlegende Datentypen

Boolean

In Go nutzen wir bool, um Booleans (Wahrheitswerte) auszudrücken, die entweder den Zustand true oder false annehmen können, wobei false der Standardwert ist. ( Du kannst übrigens Nummern zu Booleans und umgekehrt konvertieren! )

// Beispielcode
var isActive bool  // Globale Variable
var enabled, disabled = true, false  // Der Datentyp wird ausgelassen
func test() {
    var available bool  // Lokale Variable
    valid := false      // Kurzschreibweise einer Definition
    available = true    // Eine Variable deklarieren
}

Numerische Datentypen

Der Datentyp Integer (ganze Zahlen) kann sowohl den positiven, als auch den negativen Zahlenraum umfassen, was durch ein Vorzeichen kenntlich gemacht wird. Go besitzt int und uint, welche den selben Wertebereich haben. Dessen Größe hängt aber vom Betriebssystem ab. Es werden 32-Bit unter 32-Bit Betriebssystemen verwendet und 64-Bit unter 64-Bit Betriebssystemen. Go umfasst außerdem Datentypen mit einer spezifischen Länge: rune, int8, int16, int32, int64, byte, uint8, uint16, uint32 und uint64. Bedenke, dass rune ein Alias für int32 ist und byte dem uint8 gleicht.

Eine wichtige Sache, die Du wissen solltest, ist, dass Du verschiedene Datentypen nicht vermischen kannst, da es sonst zu Fehlern bei der Kompilierung kommt.

var a int8

var b int32

c := a + b

Obwohl int32 einen größeren Wertebereich als int8 abdeckt, und beide vom Typ int sind, kannst Du sie nicht miteinander kombinieren. ( c wird hier der Typ int zugewiesen )

Floats (Gleitkommazahlen) haben entweder den Datentyp float32 oder float64, aber es gibt keinen Datentyp namens float. float64 wird standardmäßig verwendet, sollte die Kurzschreibweise für eine Variablendekleration genutzt werden.

War das schon alles? Nein! Go unterstützt auch komplexe Zahlen. complex128 (bestehend aus 64-Bit für den realen Anteil und weiteren 64-Bit für den imaginären Teil) ist der Standarddatentyp. Solltest Du einen kleineren Wertebereich benötigen, kannst complex64 als Datentyp verwenden (mit 32-Bit für den realen, und nochmals 32-Bit für den imaginären Teil). Die Schreibweise lautet RE+IMi, wo RE für den realen Teil steht, und IM den Imaginären Part repräsentiert. Das i am Ende ist die imaginäre Zahl. Hier ist ein Beispiel für eine komplexe Zahl.

var c complex64 = 5+5i
// Ausgabe: (5+5i)
fmt.Printf("Der Wert ist: %v", c)

Strings

Wir sprachen vorhin darüber, das Go eine native UTF-8 Unterstützung mit sich bringt. Strings (Zeichenketten) werden durch Anführungszeichen "" gekennzeichet, oder durch Backticks (rückwärts geneigtes Hochkomma) `` .

// Beispielcode
var frenchHello string  // Grundlegende Schreibweise zur Definition eines Strings
var emptyString string = ""  // Definiert einen leeren String
func test() {
    no, yes, maybe := "no", "yes", "maybe"  // Kurzschreibweise
    japaneseHello := "Ohaiou"
    frenchHello = "Bonjour"  // Grundlegende Deklaration
}

Es ist unmöglich, die Zeichen eines Strings anhand ihres Index zu verändern. Du wirst eine Fehlermeldung erhalten, solltest Du dies ausprobieren.

var s string = "Hallo"
s[0] = 'c'

Aber was ist, wenn ich wirklich nur ein Zeichen in einem String ändern möchte? Probieren wir es mit mit diesem Codebeispiel.

s := "hello"
c := []byte(s)  // Konvertiere den String zum Typ []byte
c[0] = 'c'
s2 := string(c)  // Wandle den Wert in eine String zurück
fmt.Printf("%s\n", s2)

Nutze den + Operator, um zwei Strings zu verknüpfen.

s := "Hallo"
m := " Welt"
a := s + m
fmt.Printf("%s\n", a)

oder auch

s := "Hallo"
s = "c" + s[1:] // Du kannst die Werte mit Hilfe des Index nicht verändern, aber sie abfragen
fmt.Printf("%s\n", s)

Was ist, wenn ein String über mehrere Zeilen verlaufen soll?

m := `Hallo
Welt`

\`` wird die Zeichen in einem String escapen (d.h. mit` dessen Ausführung verhindern).

Fehlermeldungen

Go besitzt mit error einen eigenen Datentyp, um mit Fehlermeldungen umzugehen. Zudem gibt es auch ein Paket mit dem Namen errors, um weitere Möglichkeiten bereitzustellen, Fehlermeldungen zu verarbeiten.

err := errors.New("emit macho dwarf: elf header corrupted")
if err != nil {
    fmt.Print(err)
}

Grundeliegende Datenstrukturen

Die folgende Grafik enstammt dem Artikel Datenstrukturen in Go (auf englisch) aus Russ Coxs Blog. Wie Du sehen kannst, nutzt Go Abschnitte des Arbeitsspeichers, um Daten zu speichern.

Abbildung 2.1 Gos grundlegende Datenstrukturen

Einige Fähigkeiten

Gruppierte Definition

Wenn Du mehrere Konstanten und Variablen deklarieren oder Pakete importieren möchtest, kannst Du dies auch gruppiert durchführen.

Übliche Vorgehensweise.

import "fmt"
import "os"

const i = 100
const pi = 3.1415
const prefix = "Go_"

var i int
var pi float32
var prefix string

Gruppierter Ansatz.

import(
    "fmt"
    "os"
)

const(
    i = 100
    pi = 3.1415
    prefix = "Go_"
)

var(
    i int
    pi float32
    prefix string
)

Wird innerhalb von constant() einer Konstanten das Schlüsselwort iota als Wert zugewiesen, hat sie den Wert 0. Werden den folgenden Konstanten keine expliziten Werte zugewiesen, wird der letzte zugeweise Wert von iota um 1 erhöht und der folgenden Konstante zugewiesen. Dieses Verhalten beleuchten wir im folgenden Absatz.

Aufzählen mit iota

Go besitzt das Schlüselwort iota, um eine Aufzählung zu starten. Der Startwert ist 0 und wird jeweils um 1 erhöht.

const(
    x = iota  // x == 0
    y = iota  // y == 1
    z = iota  // z == 2
    w  // Folgt dem Namen der Konstante keine Deklaration, so wird die zuletzt erfolgte verwendet. w = iota wird somit implizit auf iota gesetzt. Daher gilt w == 3. Folglich kannst Du  bei x und y "= iota" einfach auslassen.
)

const v = iota // Sobald iota erneut auf `const` trifft, wird erneut mit `0` gestartet, also gilt v = 0.

const (
  e, f, g = iota, iota, iota // e, f und g haben den selben Wert 0, da sie in der selben Zeile stehen.
)

Einige Regeln

Der Grund, warum Go so prägnant ist, liegt im vorhersehbaren Verhalten der Programmiersprache.

  • Jede Variable, die mit einem großgeschriebenen Buchstaben anfängt, kann exportiert werden. Andernfalls ist sie privat.
  • Die selbe Regel gilt auch für Funktionen und Konstanten. Schlüsselwörter wie public oder private existieren in Go nicht.

Array, Slice, Map

Array

Ein array ist eine Aneinanderreihung von Daten, die wie folgt definiert wird:

var arr [n]Datentyp

wobei in [n]Datentyp das n die Länge des Arrays angibt. Datentyp ist selbsterklärend der Datentyp der Elemente (bzw. der aneinandergereihten Daten). Wie in anderen Programmiersprachen nutzen wir [], um Daten im Array zu speichern oder um sie auszulesen.

var arr [10]int  // Ein Array vom Typ [10]int
arr[0] = 42      // Der erste Index des Arrays ist 0
arr[1] = 13      // Einem Element wird ein Wert zugewiesen
fmt.Printf("Das erste Element ist %d\n", arr[0])   // Beim Auslesen des Wertes wird 42 zurückgegeben
fmt.Printf("Das letzte Element ist %d\n", arr[9]) // Es gibt den Standardwert des 10. Elements zurück, was in diesem Fall 0 ist.

Da die Länge ein Teil des Array-Typs ist, sind [3]int und [4]int verschieden, sodass wir die Länge eines Arrays nicht ändern können. Nutzt Du Arrays als Argument in einer Funktion, dann wird eine Kopie des Arrays übergeben, statt einem Zeiger (bzw. ein Verweis) auf das Original. Möchtest Du stattdessen den Zeiger übergeben, dann musst Du einen slice verwenden. Darauf gehen wir aber später nochmals ein.

Es ist möglich, := zu nutzen, um ein Array zu deklarieren.

a := [3]int{1, 2, 3} // Deklariere ein Array vom Typ int mit drei Elementen
b := [10]int{1, 2, 3} // Deklariere ein int-Array mit zehn Elementen, bei dem die ersten Drei einen Wert zugewiesen bekommen. Der Rest erhält den Standardwert 0.
c := [...]int{4, 5, 6} // Nutze `…` statt der Längenangabe. Go wird die Länge dann selbst bestimmen.

Vielleicht möchtest Du auch Arrays als Element in einem Array nutzen. Schauen wir mal, wie das geht.

// Deklariere ein zweidimensionales Array mit zwei Elementen, welche jeweils vier Elemente besitzen.
doubleArray := [2][4]int{[4]int{1, 2, 3, 4}, [4]int{5, 6, 7, 8}}


// Die Dekleration kann auch ein wenig kompakter geschrieben werden.
easyArray := [2][4]int{{1, 2, 3, 4}, {5, 6, 7, 8}} 

Arrays sind grundlegende Datenstrukturen in Go.

Abbildung 2.2 Die Zuordnung in einem mehrdimensionalen Array

Slice

In vielen Situationen ist ein Array als Datentyp keine gute Wahl, wenn wir während der Deklaration dessen Länge noch nicht wissen. Daher brauchen wir so etwas wie ein "dynamisches Array" mit einer variabler Länge. Diese werden in Go slice genannt.

slice ist nicht wirklich ein dynamisches Array. Es ist vielmehr ein Zeiger auf ein darunterliegendes array mit einer ähnlichen Deklaration, dass aber keine Länge benötigt.

// Man deklariert ein Slice wie ein Array, lässt jedoch die Länge weg.
var fslice []int

Nun deklarieren wir ein slice und vergeben Startwerte.

slice := []byte {'a', 'b', 'c', 'd'}

slice kann existierende Slices oder Arrays verändern. slice nutzt array[i:j] zum "Herrausschneiden" von Elementen. i gibt den Index des Startpunkts an und kopiert alle Elemente bis zum Index j. Beachte, dass array[j] nicht in dem Ausschnitt enthalten ist, da das Slice eine Länge von j-i hat.

// Deklariere ein Array mit der Länge 10 von vom Typ byte
var ar = [10]byte {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}

// Erstelle zwei Slices vom Typ []byte
var a, b []byte

// 'a' verweist auf die Elemente zwischen Index 3 und 5 im Array ar.
a = ar[2:5]
// 'a' besitzt nun die Elemente ar[2],ar[3] und ar[4]

// 'b' ist ein weiterer Ausschnitt (Slice) vom Array ar
b = ar[3:5]
// 'b' besitzt nun die Elemente ar[3] und ar[4]

Beachte die Unterscheide bei der Deklaration von slice und array. Wir nutzen […], um Go die Länge automatisch herausfinden zu lassen, aber nutzen [] lediglich zur Deklaration von Slices.

Ihre zugrundeliegenden Datentypen.

Abbildung 2.3 Der Zusammenhang zwischen Slices und Arrays

Slices haben bestimmte Anwendungsgebiete.

  • Ein slice beginnt mit dem Index 0, ar[:n] gleicht ar[0:n].
  • Der zweite Index gibt die Länge vom Slice an, wenn er ausgelassen wird. ar[n:] gleicht ar[n:len(ar)].
  • Du kannst auch ar[:] nutzen, um einen gesamtes Array zu kopieren, wie in den ersten beiden Punkten erklärt.

Mehr Beispiele zu slice

// Deklariere ein Array
var array = [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
// Deklariere zwei Slices
var aSlice, bSlice []byte

// Einige gewöhnliche Anwendungsfälle
aSlice = array[:3] // ist das Gleiche wie aSlice = array[0:3]. aSlice hat die Elemente a,b,c
aSlice = array[5:] // ist das Gleiche wie aSlice = array[5:10]. aSlice hat die Elemente f,g,h,i,j
aSlice = array[:]  // ist das Gleiche wie aSlice = array[0:10]. aSlice beinhaltet alle Elemente

// Ein Slice vom Slice
aSlice = array[3:7]  // aSlice hat die Elemente d,e,f,g,Anfang=4,Kapazität=7
bSlice = aSlice[1:3] // bSlice beinhaltet aSlice[1], aSlice[2], also hat es die Elemente e,f
bSlice = aSlice[:3]  // bSlice beinhaltet aSlice[0], aSlice[1], aSlice[2], also hat es die Elemente d,e,f
bSlice = aSlice[0:5] // Der Slice ist nun länger, sodass bSlice d,e,f,g,h beinhaltet
bSlice = aSlice[:]   // bSlice hat nun die gleiche Elemente wie aSlice, also d,e,f,g

slice ist ein Datentyp mit einem Zeiger, sodass Änderungen am Slice auch andere Variablen verändern, die wiederum selbt auf den Slice verweisen. Wie im oberigen Fall von aSlice und bSlice: veränderst Du den Wert eines Elements in aSlice, wird sich dieser auch im bSlice ändern.

slice ist ähnlich wie ein Struct und besteht aus drei Komponenten:

  • Ein Zeiger, der auf den Beginn des slice bzw. zugrundeliegenden Array verweist.
  • Die Länge definiert den Ausschnitt des zugrundeliegenden Arrays.
  • Kapazität definiert die max. Größe des zugrundeliegenden Arrays.

      Array_a := [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
      Slice_a := Array_a[2:5]
    

Die zugrundeliegenden Datenstrukturen vom vorherigen Code sehen wie folgt aus:

Abbildung 2.4 Arrayelemente eines Slice

Es existieren eingebaute Funktionen, um mit Slices zu arbeiten.

  • len gibt die Länge eines slice bzw. Ausschnitts zurück.
  • cap gibt die max. Länge (Kapazität) eines slice zurück.
  • append erweitert den slice um ein oder mehrere Elemente, und gibt einen slice zurück.
  • copy erstellt eine Kopie eines Slices, und gibt die Anzahl der kopierten Elemente zurück.

Achtung: append wird das Array, aus das slice verweist verändern, sowie andere Slices, die auf das selbe Array verweisen. Sollte de Kapazität des zugrundeliegenden Arrays nicht ausreichen ((cap-len) == 0), dann gibt append ein neues Array zurück. Dies hat aber keine Auswirkungen auf andere Slices, die auf das alte Array verwiesen.

Map

map verhält sich wie ein Dictionary in Python. Nutze das Schema map[SchlüsselTyp]WerteTyp, um es zu deklarieren.

Schauen wir uns ein wenig Code an. Die 'set'- und 'get'-Werte in map sind der von slice sehr ähnlich. In einem Slice kann der Datentyp des Index nur ein int sein. In einer map kann es sich jedoch um einen int, string oder um jeden anderen Datentyp handeln. Du kannst auch == und != verwenden, um Werte mit einander zu vergleichen.

// Nutze 'string' als Datentyp für den Schlüssel, 'int' als Datentyp für den Wert und `make` zum Erstellen.
var numbers map[string] int
// Ein alternativer Weg zum Deklarieren
nummern := make(map[string]int)
nummern["eins"] = 1  // Weise ein Wert durch einen Schlüssel zu
nummern["zehn"] = 10
nummern["drei"] = 3

fmt.Println("Die dritte Nummer lautet: ", nummern["drei"]) // Lese den Wert aus
// Ausgabe: Die dritte Nummer lautet: 3

Einige Anmerkungen zur Nutzung von maps.

  • map ist ungeordnet. Jedesmal, wenn Du eine map ausgeben willst, erhälst Du ein anderes Ergebnis. Dadurch ist es unmöglich, Werte über den index abzufragen. Nutze dafür den entsprechenden Schlüssel.
  • map hat keine feste Länge. Dieser Datentyp ist wie slice lediglich ein Verweis.
  • len funktioniert bei map auch. Es werden die Anzahl der Schlüssel zurückgegeben.
  • Es ist ziemlich einfach, der Wert in einer map zu ändern. Nutze nummern["eins"]=11, um den Schlüssel one den Wert 11 zuzuweisen.

Du kannst das Schema Schlüssel:Wert nutzen, um eine map mit Startwerten zu erstellen. map hat auch eingebaute Funktionen, um die Existenz eines key zu überprüfen.

Benutze delete, um ein Element in map zu löschen.

// Erstelle eine map
bewertung := map[string]float32 {"C":5, "Go":4.5, "Python":4.5, "C++":2 }
// Map hat zwei Rückgabewerte. Der zweite Rückgabewert 'ok' wird, wenn der Schlüssel nicht existiert,aus false gesetzt. Andernfalls wird true zurückgegeben.
csharpBewertung, ok := bewertung["C#"]
if ok {
    fmt.Println("C# ist in der map hat die Bewertung ", csharpBewertung)
} else {
fmt.Println("Es konnte keine Bewertung für C# in der map gefunden werden.")
}

delete(bewertung, "C")  // Lösche das Element mit dem Schlüssel "c"

Wie ich bereits sagte, ist map lediglich ein Verweis. Verweisen zwei maps auf die gleiche, zugrundeliegende Datenstruktur, wird eine Änderung Auswirkungen auf beide maps haben.

m := make(map[string]string)
m["Hallo"] = "Bonjour"
m1 := m
m1["Hallo"] = "Salut"  // Nun ist der Wert von m["hello"] Salut

make, new

make reserviert Speicher für die eingebauten Datentypen, wie map, slice, und channel, wo hingegen new für selbstdefinierte Datentype (durch type definiert) Speicher zuweist.

new(T) ordnet dem Datentyp T Speicherplatz zu und initialisiert diesen mit den Standardwerten des jeweiligen Datentyps (z.B. false für einen bool). Anschließend wird die Adresse des Speicherortes in des Datentyps *T zurückgegeben. Nach der Definition von Go ist dies ein Zeiger, welcher auf die Standardwerte der Initilisierung von T verweist.

new gibt Zeiger als Wert zurück.

Die eingebaute Funktion make(T, args) hat einen anderen Zweck als new(T). make kann für slice, map, und channel genutzt werden und gibt den Datentyp T mit seinem definierten Startwert zurück. Der Grund liegt darin, dass das ein Objekt vom zugrundeliegenden Datentyp erst erstellt wird, bevor auf diesen verwiesen werden kann. Dies ist bei diesen drei Datentypen der Fall. Zum Beispiel beinhaltet slice einen Zeiger, der auf das zugrundeliegende Array, die Länge und die Kapazität verweist. Vor der Initialisierung der Daten beinhaltet slice jedoch nur nil. Daher vergibt make den Datentypen slice, map und channel geeignetere Werte.

make gibt dabei die angesprochenen Standardwerte der entsprechenden Datentypen zurück (z.B. false für einen bool).

Die folgende Grafik verdeutlicht den Unterschied zwischen new und make.

Abbildung 2.5 Wie make und new Datenstrukturen Speicherplatz zuweisen

Standardwerte besitzen einen Wert. Dies sind die gebräuchlichsten Anwendungsfälle. Hier eine kleine Liste von Standardwerten.

int     0
int8    0
int32   0
int64   0
uint    0x0
rune    0 // rune ist ein Alias für int32
byte    0x0 // byte ist ein Alias für uint8
float32 0 // Die Größe beträgt 4 Byte
float64 0 // Die Größe beträgt 8 Byte
bool    false
string  ""

results matching ""

    No results matching ""