Як і багато інших мов, Go має вказівники. Якщо ми тільки починаємо знайомитися з вказівниками, вони можуть здаватися трохи загадковими, але щойно ми до них звикнемо, вони стануть цілком зрозумілими. Це надзвичайно важлива частина Go, тож варто приділити час, щоб справді їх зрозуміти.
Перш ніж заглиблюватися в деталі, варто зрозуміти, для чого потрібні вказівники. Вказівники дають змогу ділитися памʼяттю з іншими частинами нашої програми, і це корисно з двох основних причин:
Уявімо, що ми маємо звичайну змінну цілого типу a:
var a int
Коли ми оголошуємо змінну, Go має знайти місце в памʼяті, щоб зберегти її значення. Здебільшого це від нас приховано: коли нам потрібно дістати значення, що зберігається в цій ділянці памʼяті, ми можемо просто звернутися до нього за іменем змінної.
Наприклад, коли ми пишемо a + 2, ми фактично дістаємо значення, збережене в памʼяті, повʼязаній зі змінною a, і додаємо до нього 2.
Так само, коли нам потрібно змінити значення в ділянці памʼяті a, ми можемо скористатися іменем змінної, щоб виконати присвоєння:
a = 3
Ділянка памʼяті, повʼязана з a, тепер зберігатиме значення 3.
Хоча змінні дають нам змогу посилатися на значення в памʼяті, іноді корисно знати адресу памʼяті, на яку вказує змінна. Вказівники зберігають адреси памʼяті цих значень. Щоб оголосити змінну типу вказівника, потрібно поставити зірочку перед базовим типом:
var p *int // 'p' contains the memory address of an integer
Тут ми оголошуємо змінну p типу «вказівник на int» (*int).
Це означає, що p зберігатиме адресу памʼяті цілого числа.
Нульовим значенням вказівників є nil, бо вказівник зі значенням nil не зберігає жодної адреси памʼяті.
Щоб знайти адресу памʼяті значення змінної, ми можемо скористатися оператором &.
Наприклад, якщо ми хочемо знайти адресу памʼяті змінної a і зберегти її у вказівнику p, ми можемо зробити так:
var a int
a = 2
var p *int
p = &a // the variable 'p' contains the memory address of 'a'
Коли ми маємо вказівник, нам може знадобитися дізнатися значення, збережене за адресою памʼяті, яку представляє вказівник.
Це можна зробити за допомогою оператора *:
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
Операція *p дістає значення, збережене за адресою памʼяті, що міститься в p.
Цю операцію часто називають «розіменуванням».
Ми також можемо скористатися оператором розіменування, щоб присвоїти нове значення за адресою памʼяті, на яку посилається вказівник:
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!
Присвоєння до *pa змінить значення, збережене за адресою памʼяті, яку містить pa.
Оскільки pa містить адресу памʼяті змінної a, присвоюючи до *pa, ми фактично змінюємо значення a!
Але застереження: перш ніж розіменовувати вказівник, завжди перевіряймо, чи він не nil.
Розіменування вказівника зі значенням nil призведе до аварійного завершення програми під час виконання!
var p *int // p is nil initially
fmt.Println(*p)
// panic: runtime error: invalid memory address or nil pointer dereference
Досі ми бачили лише вказівники на прості значення. Ми також можемо створювати вказівники на структури:
type Person struct {
Name string
Age int
}
var peter Person
peter = Person{Name: "Peter", Age: 22}
var p *Person
p = &peter
Ми могли б також створити нову Person і одразу зберегти вказівник на неї:
var p *Person
p = &Person{Name: "Peter", Age: 22}
Коли ми маємо вказівник на структуру, нам не потрібно розіменовувати його, перш ніж звертатися до одного з полів:
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
Зрізи та мапи належать до особливих типів, бо вже мають вказівники у своїй реалізації. Це означає, що здебільшого нам не потрібно створювати вказівники на ці типи, щоб ділитися адресою памʼяті їхніх значень. Уявімо, що ми маємо функцію, яка збільшує значення ключа в мапі:
func incrementPeterAge(m map[string]int) {
m["Peter"] += 1
}
Якщо ми створимо мапу й викличемо цю функцію, зміни, які функція внесла до мапи, збережуться після її завершення. Таку саму поведінку ми отримали б, якби використовували вказівник, але звернімо увагу, що в цьому прикладі ми не використовуємо ні взяття адреси, ні розіменування, ні будь-який інший синтаксис вказівників:
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!
Те саме стосується зміни наявного елемента у зрізі.
Однак дії, що повертають новий зріз, як-от append, це особливий випадок, і вони можуть не змінити зріз за межами функції.
Це повʼязано з тим, як зрізи працюють усередині, але ми не розглядатимемо це докладно в цій вправі, бо це складніша тема.
Якщо нам справді цікаво, можна прочитати більше про це в Go Blog: Mechanics of 'append'
У місцевій школі неподалік дуже активна асоціація учнів. Асоціацією учнів керує президент, і раз на 2 роки проводяться вибори, щоб обрати нового президента.
На цьогорічних виборах вирішили, що потрібна нова цифрова система для підрахунку голосів. Школі потрібна наша допомога, щоб створити цю нову систему.
Новій системі голосування передусім потрібен лічильник голосів. Цей лічильник дає змогу відстежувати кількість голосів, які має певний кандидат.
Створіть функцію NewVoteCounter, яка приймає початкову кількість голосів кандидата і повертає вказівник на int, ініціалізований цією кількістю початкових голосів.
var initialVotes int
initialVotes = 2
var counter *int
counter = NewVoteCounter(initialVotes)
*counter == initialVotes // true
Тепер у нас є спосіб створювати нові лічильники! Але тепер ми розуміємо, що новій системі також знадобиться спосіб отримувати кількість голосів із лічильника.
Створіть функцію VoteCount, яка приймає лічильник (*int) як аргумент і повертає кількість голосів у лічильнику.
Якщо лічильник дорівнює nil, вважайте, що в ньому немає голосів:
var votes int
votes = 3
var voteCounter *int
voteCounter = &votes
VoteCount(voteCounter)
// => 3
var nilVoteCounter *int
VoteCount(nilVoteCounter)
// => 0
Нарешті настав час підрахувати голоси! Тепер нам потрібен спосіб збільшувати кількість голосів у лічильнику.
Створіть функцію IncrementVoteCount, яка приймає лічильник (*int) як аргумент і число голосів, і збільшує лічильник на це число голосів.
Можна вважати, що переданий вказівник ніколи не буде nil.
var votes int
votes = 3
var voteCounter *int
voteCounter = &votes
IncrementVoteCount(voteCounter, 2)
votes == 5 // true
*voteCounter == 5 // true
Тепер, коли всі голоси підраховано, настав час підготувати оголошення результатів для всієї школи. Для цього ми помічаємо, що самих лише лічильників голосів недостатньо. Потрібен спосіб повʼязати кількість голосів із певним кандидатом.
Створіть функцію NewElectionResult, яка приймає імʼя кандидата та кількість його голосів і повертає новий результат виборів.
var result *ElectionResult
result = NewElectionResult("Peter", 3)
result.Name == "Peter" // true
result.Votes == 3 // true
Структуру результату виборів уже створено для нас, і вона визначена так:
type ElectionResult struct {
// Name of the candidate
Name string
// Votes of votes the candidate had
Votes int
}
Настав час оголосити школі нового президента!
Про нового президента оголосять на маленьких цифрових табло, які є в школі.
Повідомлення має містити імʼя нового президента та кількість голосів, які він отримав, у такому форматі: <candidate_name> (<votes>).
Ось приклад такого повідомлення: "Peter (51)".
Створіть функцію DisplayResult, яка приймає *ElectionResult як аргумент і повертає рядок тексту (англ. string) з повідомленням для показу.
var result *ElectionResult
result = &ElectionResult{
Name: "John",
Votes: 32,
}
DisplayResult(result)
// => John (32)
Щоб переконатися, що остаточні результати точні, голоси перерахували. Під час перерахунку зʼясувалося, що кількість голосів деяких кандидатів відрізнялася на одиницю.
Створіть функцію DecrementVotesOfCandidate, яка приймає остаточні результати та імʼя кандидата, кількість голосів якого потрібно зменшити на одиницю.
Остаточні результати подано у вигляді map[string]int, де ключами є імена кандидатів, а значеннями - загальна кількість їхніх голосів.
var finalResults = map[string]int{
"Mary": 10,
"John": 51,
}
DecrementVotesOfCandidate(finalResults, "Mary")
finalResults["Mary"]
// => 9
Зареєструйтеся на Exercism, щоб вивчати й опановувати Go, а також 34 концепції165 вправ та справжнє наставництво від людей, і все це безкоштовно.