Перейти до змісту

Вказівники

Вказівник зберігає адресу пам'яті змінної. Ви вже бачили побіжні згадки про & і * у 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:

var p *int
fmt.Println(p)          // <nil>
fmt.Println(p == nil)   // true

Розіменування nil-вказівника спричиняє паніку під час виконання:

var p *int
fmt.Println(*p)         // runtime error: invalid memory address or nil pointer dereference

Це найпоширеніша причина панік у Go на практиці. Завжди перевіряйте p != nil, якщо є хоч найменша ймовірність, що вказівник не ініціалізований.

new(T) — виділити пам'ять і повернути вказівник

new(T) — це вбудована функція, яка виділяє нове нульове значення типу T і повертає його адресу. Вказівник не є nil.

p := new(int)           // p is *int, *p == 0
*p = 99
fmt.Println(*p)         // 99

На практиці new зустрічається рідше, ніж &Struct{...}, оскільки складені літерали дозволяють ініціалізувати поля одночасно. new(int) зручний, коли потрібна проста змінна ціле число за вказівником.

Навіщо взагалі існують вказівники

Go зазвичай передає аргументи за значенням — функція отримує копію. Два приводи використовувати вказівник замість цього:

  1. Ви хочете, щоб функція змінила змінну виклику.
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
  1. Ви хочете уникнути копіювання великої структури. Передача вказівника переносить 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] для вказівника неможливі. Компілятор відхиляє їх цілком.

p := &x
p++                     // compile error: invalid operation: p++ (non-numeric type *int)

Якщо потрібен обхід пам'яті, схожий на вказівниковий, використовуйте зріз — його час виконання знає про межі і панікуватиме при виході за них замість пошкодження пам'яті.

Вказівники проти посилань

Деякі типи 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 }

Джерела