Go 2: Strukturen, Slices und Maps
Im ersten Teil haben wir primitive Datentypen betrachtet. Im zweiten Teil geht es um zusammengesetzte Datentypen (engl. Compound Data Types), welche sich aus primitiven und anderen zusammengesetzten Datentypen zusammensetzen können.
Strukturen
Wir haben gesehen, dass byte
bloss ein Alias für uint8
ist, was mithilfe von
type
folgendermassen bewerkstelligt wird:
type byte uint8
Man kann also beliebige bestehende Datentypen nehmen und ihnen eine zusätzliche Bezeichnung geben, beispielsweise:
type degrees float32
type year int16
type sign rune
Mehrere Variablen gleichen oder unterschiedlichen Typs können zu sogenannten Strukturen (Spec) zusammengefügt werden:
struct {
[Element] [Datentyp]
...
}
Mit einer struct
wird ein neuer Datentyp definiert. Mithilfe des
type
-Schlüsselworts (Spec) kann
diesem neuen Typ eine Bezeichnung gegeben werden:
type [Bezeichnung] struct {
[Element] [Datentyp]
...
}
Eine Struktur ist heterogen, d.h. es können darin Variablen verschiedener Datentypen gespeichert werden. In diesem Beispiel werden Informationen von einem Steckbrief in einer Struktur abgelegt:
type Person struct {
FirstName string
LastName string
DayOfBirth byte
MonthOfBirth byte
YearOfBirth int16
}
Für die Gross- und Kleinschreibung sollen bis auf Weiteres folgende Regeln gelten:
- Neue Datentypen wie Strukturen werden mit grossem Anfangsbuchstaben
geschrieben, z.B.
Person
oderAddress
. - Elemente von Strukturen werden ebenfalls mit grossem Anfangsbuchstaben
geschrieben, z.B.
FirstName
oderLastName
. - Konkrete Variablen werden mit kleinem Anfangsbuchstaben geschrieben, also
person
oderaddress
.
Eine Struktur kann auch aus anderen Strukturen zusammengesetzt werden. So lassen sich die Informationen eines Steckbriefs gruppieren:
type FullName struct {
FirstName string
LastName string
}
type BirthDate struct {
DayOfBirth byte
MonthOfBirth byte
YearOfBirth int16
}
type Person struct {
Name FullName
Born BirthDate
}
Eine Struktur kann als Variable deklariert werden:
var myName FullName
var myBirthDate BirthDate
Die Initialisierung der Struktur erfolgt mit folgender Syntax:
var myName FullName = FullName{
FirstName: "Patrick",
LastName: "Bucher",
}
var myBirthDate BirthDate = BirthDate{
DayOfBirth: 24,
MonthOfBirth: 6,
YearOfBirth: 1987,
}
Beachten Sie, dass am Ende jeder Zeile ein Komma stehen muss! (Das hat Vorteile beim Umsortieren der Angaben, und in der Versionskontrolle sieht man dadurch beim Hinzufügen einer Angabe keine Änderung auf der vorherigen Zeile durch das Hinzufügen des Kommas.)
Der Typ bei der Deklaration (links vom =
) kann weggelassen werden:
var teacher = Person{
Name: myName,
Born: myBirthDate,
}
Da die Reihenfolge der Elemente festgelegt ist, kann deren Name weggelassen werden:
var teacher = Person{
myName,
myBirthDate,
}
Lesender und schreibender Zugriff auf einzelne Elemente der Struktur ist mithilfe des Punkt-Operators möglich:
fmt.Println("Teacher's last name:", teacher.Name.LastName)
teacher.Name.FirstName = "Padraigh"
fmt.Println("Teacher's Irish name:", teacher.Name.FirstName)
Ausgabe:
Teacher's last name: Bucher
Teacher's Irish name: Padraigh
Einbetten von Strukturen
Strukturen können aus anderen Strukturen bestehen. Beim Zugriff auf die
Unterelemente muss der Zugriff entsprechend über mehrere Schritte erfolgen, also
z.B. teacher.Name.LastName
im vorherigen Beispiel.
Verzichtet man beim Einbetten von Unterstrukturen auf einen Namen, können die Unterelemente direkt angesprochen werden:
type Teacher struct {
FullName
BirthDate
TeachesModule string
}
pbucher := Teacher{
myName,
myBirthDate,
"Modul 346",
}
fmt.Println("Teacher's full name:", pbucher.FirstName, pbucher.LastName)
fmt.Printf("Teacher's birth date: %d.%d.%d\n",
pbucher.DayOfBirth, pbucher.MonthOfBirth, pbucher.YearOfBirth)
Ausgabe:
Teacher's full name: Patrick Bucher
Teacher's birth date: 24.6.1987
Ausgabe von Strukturen
Mit der Formatangabe %v
kann eine Struktur direkt mit allen ihren Elementen
ausgegeben werden. Mit der Formatangabe %q
erfolgt die Ausgabe in Go-Syntax:
fmt.Printf("%v\n", pbucher)
fmt.Printf("%q\n", pbucher)
Ausgabe:
{{Patrick Bucher} {24 6 1987} Modul 346}
{{"Patrick" "Bucher"} {'\x18' '\x06' '߃'} "Modul 346"}
(Offenbar wurden die Angaben des Geburtsdatums auf der zweiten Zeile als
hexadezimale bzw. Unicode-Zeichen interpretiert. Die generische Ausgabe mit %q
ist zwar sehr einfach, aber nicht in jedem Fall sehr hilreich.)
Slices
Slices (Spec) sind homogen, d.h. es können darin Werte des gleichen Datentyps abgespeichert werden. Im Gegensatz zu einer Struktur können das (theoretisch) beliebig viele Werte sein; die verfügbare Menge an Arbeitsspeicher ist die einzige Grenze.
Strenggenommen stellen Slices nur eine Sicht auf Arrays (Spec) dar, welche die eigentlichen Daten beinhalten. Für unsere Zwecke ist aber diese Unterscheidung nicht notwendig. Darum soll hier nur von Slices die Rede sein.
Ein Slice wird wie eine Variable deklariert. Dem Typ geht aber ein eckiges Klammernpaar voraus:
var name string // a single string
var names []string // a slice of strings
Bei der Initialisierung kann wiederum auf die Typangabe links vom =
verzichtet
werden, da diese rechts davon angegeben wird:
var days = []string{"Mo", "tu", "We", "Th", "Fr", "Sa"}
Beim days
-Slice ging offenbar der Sonntag vergessen, der mithilfe von append
ans Ende des Slices angefügt werden kann:
days = append(days, "Su")
Achtung: append
gibt eine Referenz auf ein neues Slice zurück, weswegen der
Rückgabewert wiederum abgespeichert werden muss.
Das zweite Element "tu"
wurde im Gegensatz zu den anderen Elementen
kleingeschrieben. Dies soll angepasst werden, indem das Element mithilfe des
0-basierenden Index überschrieben wird:
days[1] = "Tu"
Das Slice sollte nun die abgekürzten Wochentage enthalten. Deren Anzahl kann mit
der eingebauten len()
-Funktion ermittelt werden:
fmt.Println(days)
fmt.Println(len(days))
Ausgabe:
[Mo Tu We Th Fr Sa Su]
7
Das erste Element ist an Index 0
zu finden. Für den Zugriff auf das letzte
Element kann die Länge vom Slice abzüglich eins verwendet werden:
firstDay := days[0]
lastDay := days[len(days)-1]
fmt.Println("from", firstDay, "to", lastDay)
Ausgabe:
from Mo to Su
Slice-Ausschnitte und Slicing-Syntax
Mit der namensgebenden Slicing-Syntax können Ausschnitte aus dem Slice ermittelt werden. Hierzu wird die Untergrenze inklusiv, die Obergrenze exklusiv angegeben, sodass die Obergrenze von der Untergrenze subtrahiert die gleich der Länge des neuen Slices ist:
workdays := days[0:5]
weekend := days[5:7]
fmt.Println(workdays, len(workdays))
fmt.Println(weekend, len(weekend))
Ausgabe:
[Mo Tu We Th Fr] 5
[Sa Su] 2
Exkurs: Länge und Kapazität (optional)
Mithilfe der eingebauten make
-Funktion lassen sich verschiedene
Datenstrukturen erzeugen, z.B. Slices. Hierzu wird ein Typ und die initiale
Grösse des Slices angegeben:
var numbers = make([]int, 0)
var moreNumbers = make([]int, 3)
Ein Slice hat neben einer Länge (len()
) auch eine Kapazität (cap()
) für
weitere Elemente:
fmt.Println(numbers, len(numbers), cap(numbers))
fmt.Println(moreNumbers, len(moreNumbers), cap(moreNumbers))
Ausgabe:
[] 0 0
[0 0 0] 3 3
Das erste Slice (numbers
) enthält keine Werte und hat darum die Länge und
Kapazität 0. Das zweite Slice besteht aus drei Werten und hat darum die Länge
und Kapazität 3.
Der Unterschied zwischen Länge und Kapazität wird erst dann klar, wenn man mithilfe der Slicing-Syntax auf einen Unterbereich zugreift:
var extract = moreNumbers[0:2]
fmt.Println(extract, len(extract), cap(extract))
Ausgabe:
[0 0] 2 3
Die Länge beträgt jetzt nur noch 2
, doch die Kapazität beträgt nach wie vor
3
, weil das zugrundeliegende Slice (bzw. Array) noch einen weiteren Wert
aufnehmen kann.
Maps
Maps (Spec) speichern Informationen als
Schlüssel-Wert-Paar (key-value pair) ab. (In anderen Programmiersprachen
werden Maps als Dictionary, Assoziatives Array, Hash oder Table
bezeichnet.) Sie können als Verallgemeinerung von Slices (bzw. Arrays) gesehen
werden, da man als Index nicht nur Zahlen von 0
bis n-1
(wenn n
die Länge
ist), sondern beliebige Werte verwenden kann.
Eine Map wird mit zwei Datentypen deklariert: Einen für den Schlüssel, und einen für den Wert:
var countryPopulation map[string]uint
var numbersSquareRoots map[int]float32
var numbersIsPrime map[int]bool
countryPopulation
verwendetstring
als Schlüssel unduint
als Wert.- Das Land wird mit einem
string
bezeichnet. - Die Anzahl Einwohner werden mit
uint
angegeben.
- Das Land wird mit einem
numbersSquareRoots
verwendetint
als Schlüssel undfloat32
als Wert.- Es werden Ganzzahlen als Schlüssel abgespeichert.
- Die Quadratwurzel einer ganzen Zahl muss hingegen keine ganze Zahl sein.
numbersIsPrime
verwendetint
als Schlüssel undbool
als Wert.- Es werden wiederum Ganzzahlen als Schlüssel abgespeichert.
- Der
bool
-Wert gibt an, ob es sich bei einer Zahl um eine Primzahl handelt.
Maps sind offenbar sehr flexibel und vielseitig. Brian Kernighan, der das Standardwerk über Go mitgeschrieben hat, bezeichnet assoziative Arrays (d.h. Maps) als eine der wichtigsten Datenstrukturen überhaupt und erklärt diese sehr verständlich.
Maps erstellen
Maps können auch mithilfe von make
und einer initialen Kapazität erstellt
werden:
countryPopulation := make(map[string]uint, 0)
numbersSquareRoots := make(map[int]float32, 0)
numbersIsPrime := make(map[int]bool, 0)
Maps können mit Anfangswerten belegt werden, indem Schlüssel und Wert durch einen Doppelpunkt voneinander getrennt werden:
countryPopulation := map[string]uint{
"AT": 8_917_000,
"CH": 8_637_000,
"DE": 83_240_000,
}
numbersSquareRoots := map[int]float32{
1: 1.0,
2: 1.41421356237,
4: 2.0,
}
numbersIsPrime := map[int]bool{
1: false,
2: true,
3: true,
4: false,
}
Werte einfügen, herauslesen und entfernen
Weitere Elemente können direkt unter Angabe eines Schlüssel und Wertes in ein Dictionary eingefügt werden:
countryPopulation["IT"] = 59_550_000
numbersSquareRoots[16] = 4.0
numbersIsPrime[13] = true
Auf bestehende Elemente kann man mit der gleichen Syntax zugreifen:
fmt.Println("Swiss Population:", countryPopulation["CH"])
fmt.Println("Square Root of 16:", numbersSquareRoots[16])
fmt.Println("Is 13 Prime?", numbersIsPrime[13])
Ausgabe:
Swiss Population: 8637000
Square Root of 16: 4
Is 13 Prime? true
Beim Zugriff mit Schlüsseln, die nicht existieren, wird der Nullwert zurückgegeben und kein Fehler geworfen:
fmt.Println("French Population:", countryPopulation["FR"])
Ausgabe:
French Population: 0
Weist man den Wert des Zugriffs einer Variablen zu, kann man in einer zweiten Variablen erfahren, ob der Wert tatsächlich vorhanden war:
frenchPopulation, ok := countryPopulation["FR"]
fmt.Println("Value:", frenchPopulation)
fmt.Println("Was stored in map?", ok)
Ausgabe:
French Population: 0
Value: 0
Was stored in map? false
Mithilfe der eingebauten delete()
-Funktion kann ein Element anhand seines
Schlüssels aus der Map entfernt werden:
delete(countryPopulation, "FR")
fmt.Println(countryPopulation)
Ausgabe:
map[AT:8917000 CH:8637000 DE:83240000 IT:59550000]
Structs, Slices und Maps kombiniert
Die zusammengesetzten Datentypen struct
, slice
und map
können praktisch
beliebig miteinander kombiniert werden:
- Eine Struktur kann Slices und Maps enthalten.
- Slices können aus Strukturen und Maps bestehen.
- Maps können Strukturen und Slices ablegen.
Im folgenden Beispiel (combined/main.go
) werden folgende Datenstrukturen
verwendet:
- Eine
struct
namensPlayer
bestehend aus Vor- und Nachname. - Eine
map[byte]Player
, welche Spieler unter einer Zahl ablegt (z.B. Rückennummer eines Spielers). - Ein Slice bestehend aus
map[byte]Player
-Maps, welche mehrere solche Teams ablegen kann.
type Player struct {
FirstName string
LastName string
}
type Team map[byte]Player
teamA := Team{
1: Player{
FirstName: "Joe",
LastName: "Doe",
},
2: Player{
FirstName: "Jay",
LastName: "Day",
},
}
teamB := Team{
1: Player{
FirstName: "Jim",
LastName: "Jam",
},
2: Player{
FirstName: "Jam",
LastName: "Bam",
},
}
teams := []Team{
teamA,
teamB,
}
fmt.Println(teams)
Ausgabe:
[map[1:{Joe Doe} 2:{Jay Day}] map[1:{Jim Jam} 2:{Jam Bam}]]
Die map[byte]Player
wird als Typ Team
definiert, was ihre Wiederverwendung
im teams
-Slice einfacher macht.