Wie viele andere Sprachen hat Go Pointer. Wenn du neu bei Pointern bist, können sie etwas geheimnisvoll wirken, aber sobald du dich an sie gewöhnt hast, sind sie ganz unkompliziert. Sie sind ein entscheidender Teil von Go, also nimm dir etwas Zeit, sie wirklich zu verstehen.
Bevor wir in die Details eintauchen, lohnt es sich zu verstehen, wofür Pointer verwendet werden. Pointer sind eine Möglichkeit, Speicher mit anderen Teilen unseres Programms zu teilen, was aus zwei wichtigen Gründen nützlich ist:
Nehmen wir an, wir haben eine gewöhnliche Variable a vom Typ Ganzzahl:
var a int
Wenn wir eine Variable deklarieren, muss Go einen Platz im Speicher finden, um ihren Wert zu speichern. Das ist weitgehend von uns abstrahiert. Wenn wir den Wert abrufen müssen, der an dieser Speicherstelle gespeichert ist, können wir ihn einfach über den Variablennamen ansprechen.
Wenn wir zum Beispiel a + 2 schreiben, rufen wir im Grunde den Wert ab, der im Speicher der Variable a gespeichert ist, und addieren 2 dazu.
Ähnlich verhält es sich, wenn wir den Wert im Speicher von a ändern möchten: Wir können den Variablennamen verwenden, um eine Zuweisung vorzunehmen:
a = 3
Die Speicherstelle, die mit a verknüpft ist, speichert jetzt den Wert 3.
Variablen ermöglichen es uns, auf Werte im Speicher zu verweisen. Manchmal ist es jedoch nützlich, die Speicheradresse zu kennen, auf die die Variable zeigt. Pointer enthalten die Speicheradressen dieser Werte. Du deklarierst eine Variable mit einem Pointer-Typ, indem du dem zugrunde liegenden Typ ein Sternchen voranstellst:
var p *int // 'p' contains the memory address of an integer
Hier deklarieren wir eine Variable p vom Typ „Pointer auf int“ (*int).
Das bedeutet, dass p die Speicheradresse einer Ganzzahl enthält.
Der Nullwert von Pointern ist nil, weil ein nil-Pointer keine Speicheradresse enthält.
Um die Speicheradresse des Werts einer Variable zu finden, können wir den Operator & verwenden.
Wenn wir zum Beispiel die Speicheradresse der Variable a ermitteln und im Pointer p speichern möchten, können wir Folgendes tun:
var a int
a = 2
var p *int
p = &a // the variable 'p' contains the memory address of 'a'
Wenn wir einen Pointer haben, möchten wir vielleicht den Wert kennen, der an der Speicheradresse gespeichert ist, die der Pointer darstellt.
Das können wir mit dem Operator * tun:
var a int
a = 2
var p *int
p = &a // the variable 'p' contains the memory address of 'a'
var b int
b = *p // b == 2
Die Operation *p ruft den Wert ab, der an der Speicheradresse liegt, die in p gespeichert ist.
Diese Operation wird oft als „Dereferenzierung“ bezeichnet.
Wir können den Dereferenzierungsoperator auch verwenden, um der Speicheradresse, auf die der Pointer verweist, einen neuen Wert zuzuweisen:
var a int // declare int variable 'a'
a = 2 // assign 'a' the value of 2
var pa *int
pa = &a // 'pa' now contains to the memory address of 'a'
*pa = *pa + 2 // increment by 2 the value at memory address 'pa'
fmt.Println(a) // Output: 4
// 'a' will have the new value that was changed via the pointer!
Eine Zuweisung an *pa ändert den Wert, der an der Speicheradresse gespeichert ist, die pa enthält.
Da pa die Speicheradresse von a enthält, ändern wir durch die Zuweisung an *pa effektiv den Wert von a!
Ein Hinweis zur Vorsicht: Prüfe immer, ob ein Pointer nicht nil ist, bevor du ihn dereferenzierst.
Das Dereferenzieren eines nil-Pointers lässt das Programm zur Laufzeit abstürzen!
var p *int // p is nil initially
fmt.Println(*p)
// panic: runtime error: invalid memory address or nil pointer dereference
Bisher haben wir nur Pointer auf primitive Werte gesehen. Wir können auch Pointer für Structs erstellen:
type Person struct {
Name string
Age int
}
var peter Person
peter = Person{Name: "Peter", Age: 22}
var p *Person
p = &peter
Wir hätten auch eine neue Person erstellen und sofort einen Pointer darauf speichern können:
var p *Person
p = &Person{Name: "Peter", Age: 22}
Wenn wir einen Pointer auf ein Struct haben, müssen wir den Pointer nicht dereferenzieren, bevor wir auf eines der Felder zugreifen:
var p *Person
p = &Person{Name: "Peter", Age: 22}
fmt.Println(p.Name) // Output: "Peter"
// Go automatically dereferences 'p' to allow
// access to the 'Name' field
Slices und Maps sind besondere Typen, weil sie bereits Pointer in ihrer Implementierung enthalten. Das bedeutet, dass wir für diese Typen meist keine Pointer erstellen müssen, um die Speicheradresse ihrer Werte zu teilen. Stell dir vor, wir haben eine Funktion, die den Wert eines Schlüssels in einer Map erhöht:
func incrementPeterAge(m map[string]int) {
m["Peter"] += 1
}
Wenn wir eine Map erstellen und diese Funktion aufrufen, bleiben die Änderungen, die die Funktion an der Map vorgenommen hat, nach dem Ende der Funktion erhalten. Das ist ein ähnliches Verhalten, wie wir es mit einem Pointer erhalten würden. Beachte jedoch, dass wir in diesem Beispiel keine Referenzierung/Dereferenzierung oder irgendeine Pointer-Syntax verwenden:
ages := map[string]int{
"Peter": 21
}
incrementPeterAge(ages)
fmt.Println(ages)
// Output: map[Peter:22]
// The changes the function 'incrementPeterAge' made to the map are visible after the function ends!
Dasselbe gilt, wenn du ein vorhandenes Element in einem Slice änderst.
Allerdings sind Aktionen, die ein neues Slice zurückgeben, wie append, ein Sonderfall und ändern das Slice außerhalb der Funktion möglicherweise nicht.
Das liegt an der internen Funktionsweise von Slices, aber wir werden dies in dieser Übung nicht im Detail behandeln, da es ein fortgeschritteneres Thema ist.
Wenn du wirklich neugierig bist, kannst du mehr darüber im Go-Blog: Mechanics of 'append' lesen.
Eine Schule in deiner Nähe hat eine sehr aktive Schülervereinigung. Die Schülervereinigung wird von einem Präsidenten geleitet, und alle 2 Jahre finden Wahlen statt, um einen neuen Präsidenten zu wählen.
Bei der diesjährigen Wahl wurde entschieden, dass ein neues digitales System zum Auszählen der Stimmen benötigt wird. Die Schule braucht deine Hilfe beim Aufbau dieses neuen Systems.
Eines der ersten Dinge, die das neue Wahlsystem braucht, ist ein Stimmenzähler. Dieser Zähler ist eine Möglichkeit, die Stimmen eines bestimmten Kandidaten im Blick zu behalten.
Erstelle eine Funktion NewVoteCounter, die die Anzahl der anfänglichen Stimmen für einen Kandidaten entgegennimmt und einen Zeiger auf einen int zurückgibt, der mit der angegebenen Anzahl anfänglicher Stimmen initialisiert ist.
var initialVotes int
initialVotes = 2
var counter *int
counter = NewVoteCounter(initialVotes)
*counter == initialVotes // true
Jetzt hast du eine Möglichkeit, neue Zähler zu erstellen! Aber jetzt stellst du fest, dass das neue System auch eine Möglichkeit braucht, die Anzahl der Stimmen aus einem Zähler zu ermitteln.
Erstelle eine Funktion VoteCount, die einen Zähler (*int) als Argument entgegennimmt und die Anzahl der Stimmen im Zähler zurückgibt.
Wenn der Zähler nil ist, solltest du davon ausgehen, dass der Zähler keine Stimmen hat:
var votes int
votes = 3
var voteCounter *int
voteCounter = &votes
VoteCount(voteCounter)
// => 3
var nilVoteCounter *int
VoteCount(nilVoteCounter)
// => 0
Endlich ist es Zeit, die Stimmen auszuzählen! Jetzt brauchst du eine Möglichkeit, die Stimmen in einem Zähler zu erhöhen.
Erstelle eine Funktion IncrementVoteCount, die einen Zähler (*int) als Argument und eine Anzahl von Stimmen entgegennimmt und den Zähler um diese Anzahl von Stimmen erhöht.
Du kannst davon ausgehen, dass der übergebene Zeiger nie nil ist.
var votes int
votes = 3
var voteCounter *int
voteCounter = &votes
IncrementVoteCount(voteCounter, 2)
votes == 5 // true
*voteCounter == 5 // true
Da nun alle Stimmen ausgezählt sind, ist es Zeit, die Bekanntgabe der Ergebnisse für die ganze Schule vorzubereiten. Dabei fällt dir auf, dass es nicht ausreicht, nur Zähler für die Stimmen zu haben. Es muss eine Möglichkeit geben, die Anzahl der Stimmen einem bestimmten Kandidaten zuzuordnen.
Erstelle eine Funktion NewElectionResult, die den Namen eines Kandidaten und seine Anzahl an Stimmen entgegennimmt und
ein neues Wahlergebnis zurückgibt.
var result *ElectionResult
result = NewElectionResult("Peter", 3)
result.Name == "Peter" // true
result.Votes == 3 // true
Das Struct für das Wahlergebnis ist bereits für dich erstellt und ist wie folgt definiert:
type ElectionResult struct {
// Name of the candidate
Name string
// Votes of votes the candidate had
Votes int
}
Es ist Zeit, der Schule den neuen Präsidenten bekanntzugeben!
Der Präsident wird auf den kleinen digitalen Anzeigetafeln bekanntgegeben, die die Schule hat.
Die Nachricht sollte den Namen des neuen Präsidenten und die Anzahl seiner Stimmen im folgenden Format anzeigen: <candidate_name> (<votes>).
Ein Beispiel für eine solche Nachricht ist: "Peter (51)".
Erstelle eine Funktion DisplayResult, die ein *ElectionResult als Argument entgegennimmt und einen String mit der anzuzeigenden Nachricht zurückgibt.
var result *ElectionResult
result = &ElectionResult{
Name: "John",
Votes: 32,
}
DisplayResult(result)
// => John (32)
Um sicherzustellen, dass die endgültigen Ergebnisse korrekt waren, wurden die Stimmen neu ausgezählt. Bei der Neuauszählung stellte sich heraus, dass die Anzahl der Stimmen für einige Kandidaten um eins abwich.
Erstelle eine Funktion DecrementVotesOfCandidate, die die endgültigen Ergebnisse und den Namen eines Kandidaten entgegennimmt, für den du seine Stimmenzahl verringern sollst.
Die endgültigen Ergebnisse werden in Form einer map[string]int angegeben, wobei die Schlüssel die Namen der Kandidaten und die Werte ihre Gesamtstimmen sind.
var finalResults = map[string]int{
"Mary": 10,
"John": 51,
}
DecrementVotesOfCandidate(finalResults, "Mary")
finalResults["Mary"]
// => 9
Melde dich bei Exercism an, um Go mit 34 Konzepte165 Übungen und echtem menschlichen Mentoring zu lernen und zu meistern, alles kostenlos.