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
oderprivate
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]
gleichtar[0:n]
. - Der zweite Index gibt die Länge vom Slice an, wenn er ausgelassen wird.
ar[n:]
gleichtar[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 einesslice
bzw. Ausschnitts zurück.cap
gibt die max. Länge (Kapazität) einesslice
zurück.append
erweitert denslice
um ein oder mehrere Elemente, und gibt einenslice
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 einemap
ausgeben willst, erhälst Du ein anderes Ergebnis. Dadurch ist es unmöglich, Werte über denindex
abzufragen. Nutze dafür den entsprechendenSchlüssel
.map
hat keine feste Länge. Dieser Datentyp ist wieslice
lediglich ein Verweis.len
funktioniert beimap
auch. Es werden die Anzahl derSchlüssel
zurückgegeben.- Es ist ziemlich einfach, der Wert in einer
map
zu ändern. Nutzenummern["eins"]=11
, um denSchlüssel
one den Wert11
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 map
s auf die gleiche, zugrundeliegende Datenstruktur, wird eine Änderung Auswirkungen auf beide map
s 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 ""
Links
- Inhaltsverzeichnis
- Vorheriger Abschnitt: "Hallo Go"
- Nächster Abschnitt: Kontrollstrukturen und Funktionen