Вказівники¶
Вказівник зберігає адресу пам'яті змінної. Ви вже бачили побіжні
згадки про & і * у
04-operators.md; ця стаття пояснює, що вони
насправді роблять.
Два оператори, один префікс типу¶
| Синтаксис | Що означає |
|---|---|
*T |
тип «вказівник на T» — використовується в оголошеннях та підписах |
&x |
оператор взяття адреси — повертає вказівник на змінну x |
*p |
оператор розіменування — читає (або записує) значення за адресою в p |
var x int = 5
p := &x // p has type *int and points at x
fmt.Println(p) // 0xc000018058 (some address)
fmt.Println(*p) // 5 — the value through the pointer
*p = 42 // write through the pointer
fmt.Println(x) // 42 — x changed
Нульове значення вказівника — nil¶
Оголошений, але не ініціалізований вказівник дорівнює nil:
Розіменування nil-вказівника спричиняє паніку під час виконання:
Це найпоширеніша причина панік у Go на практиці. Завжди перевіряйте
p != nil, якщо є хоч найменша ймовірність, що вказівник не ініціалізований.
new(T) — виділити пам'ять і повернути вказівник¶
new(T) — це вбудована функція, яка виділяє нове нульове значення типу
T і повертає його адресу. Вказівник не є nil.
На практиці new зустрічається рідше, ніж &Struct{...}, оскільки
складені літерали дозволяють ініціалізувати поля одночасно.
new(int) зручний, коли потрібна проста змінна ціле число за вказівником.
Навіщо взагалі існують вказівники¶
Go зазвичай передає аргументи за значенням — функція отримує копію. Два приводи використовувати вказівник замість цього:
- Ви хочете, щоб функція змінила змінну виклику.
func zero(x int) { x = 0 } // operates on its own copy
func zeroP(x *int) { *x = 0 } // writes through the caller's pointer
n := 7
zero(n)
fmt.Println(n) // 7 — zero saw a copy; n unchanged
zeroP(&n)
fmt.Println(n) // 0 — zeroP wrote through the pointer
- Ви хочете уникнути копіювання великої структури. Передача вказівника переносить 8 байт; передача структури розміром 1 КБ за значенням — усі 1 КБ.
type Snapshot struct { /* many fields, big */ }
func process(s *Snapshot) { /* ... */ } // cheap call
З досвіду Python: Python не надає доступу до вказівників, але розрізняє змінювані (
list,dict, користувацькі класи) та незмінні (int,tuple,str) значення. Змінювані об'єкти Python поводяться дещо схоже до значень Go за неявним вказівником — функція та виклик спільно використовують той самий об'єкт. У Go спільне використання є явним: ви передаєте*T.
Жодної арифметики вказівників¶
На відміну від C, p++, p + 1 або p[3] для вказівника неможливі.
Компілятор відхиляє їх цілком.
Якщо потрібен обхід пам'яті, схожий на вказівниковий, використовуйте зріз — його час виконання знає про межі і панікуватиме при виході за них замість пошкодження пам'яті.
Вказівники проти посилань¶
Деякі типи Go (зрізи, мапи, канали, функції) самі по собі є
подібними до посилань під капотом: копіювання заголовка зрізу не
копіює підлягаючий масив. Тому *[]int майже ніколи не потрібен —
передача звичайного []int вже надає спільний доступ до підлягаючого масиву.
| Тип | Передача за значенням дає виклику спільний вигляд? |
|---|---|
int, bool, float64, масиви, структури |
Ні — повна копія |
string |
Так (заголовки рядків малі; дані незмінні) |
[]T (зріз) |
Так |
map[K]V |
Так |
chan T |
Так |
func(...) |
Так |
| Інтерфейси | Так (значення інтерфейсу — по суті пара (тип, *значення)) |
Використовуйте *T, коли застосовна причина зміни виклику або уникнення
копіювання, а тип і так не є подібним до посилання.
Порівняння вказівників¶
Два вказівники рівні за ==, якщо вони вказують на ту саму адресу (або
обидва є nil). Значення, на які вони вказують, не порівнюються.
a, b := 1, 1
fmt.Println(&a == &b) // false — different variables
fmt.Println(&a == &a) // true — same address
var x int
p := &x
q := &x
fmt.Println(p == q) // true
Щоб порівняти те, на що вони вказують: *p == *q.
Поширена пастка — адреса змінної циклу¶
Цикл for ... range у сучасному Go присвоює свіжу змінну на кожній
ітерації, тому взяття &v всередині циклу є безпечним. Старий код
іноді демонстрував протилежне — будьте обережні при читанні застарілих
прикладів.
nums := []int{10, 20, 30}
var ps []*int
for _, v := range nums {
ps = append(ps, &v)
}
for _, p := range ps {
fmt.Print(*p, " ") // 10 20 30 — each pointer points at its own copy
}
fmt.Println()
Короткий довідник¶
| Що потрібно | Як записати |
|---|---|
| Оголосити змінну типу вказівник | var p *int |
| Взяти вказівник на наявну змінну | p := &x |
| Прочитати або записати через вказівник | *p, *p = newValue |
| Перевірити на nil | if p != nil { ... } |
| Виділити нульове значення за вказівником | p := new(int) |
| Передати структуру дешево | func f(s *BigStruct) |
| Дозволити функції змінити змінну виклику | func reset(n *int) { *n = 0 } |