Указатели: основы
Адрес — это тоже данные
Каждая переменная живёт где-то в памяти. Указатель — это переменная, которая хранит не значение, а адрес другой переменной.
x := 42
p := &x // p хранит адрес переменной x
fmt.Println(x) // 42 — само значение
fmt.Println(&x) // 0xc000018090 — адрес x в памяти
fmt.Println(p) // 0xc000018090 — p хранит тот же адрес
fmt.Println(*p) // 42 — значение по адресу (разыменование)Два оператора:
&x— взять адрес переменнойx*p— получить значение по адресуp
Тип указателя на int записывается как *int. Указатель на string — *string. И так для любого типа.
Изменение значения через указатель
Разыменование работает в обе стороны — можно не только читать, но и писать по адресу:
x := 10
p := &x
*p = 99 // записываем 99 по адресу, который хранит p
fmt.Println(x) // 99 — оригинальная переменная измениласьp и x — разные переменные, но *p и x обращаются к одному месту в памяти.
Почему это важно: передача по значению
В Go все аргументы функции передаются по значению — функция получает копию. Это значит, что изменения внутри не затрагивают оригинал:
func double(n int) {
n = n * 2
fmt.Println("внутри:", n) // 20
}
func main() {
x := 10
double(x)
fmt.Println("снаружи:", x) // 10 — не изменился
}Чтобы функция могла изменить переменную снаружи, передайте указатель:
func double(n *int) {
*n = *n * 2
}
func main() {
x := 10
double(&x) // передаём адрес x
fmt.Println(x) // 20 — изменился!
}Функция получила адрес x и записала новое значение прямо туда.
Функция new
Стандартная функция new(T) выделяет память под тип T, инициализирует нулём и возвращает *T:
p := new(int) // *int, значение = 0
*p = 55
fmt.Println(*p) // 55
s := new(string) // *string, значение = ""
*s = "hello"
fmt.Println(*s) // helloНа практике new используется редко. Чаще берут адрес уже существующей переменной (&x) или создают структуру через &MyStruct{...}.
Указатели и структуры
Указатели особенно часто встречаются со структурами. Go позволяет не писать (*p).Field — точка автоматически разыменовывает:
type Point struct {
X, Y int
}
p := &Point{X: 3, Y: 4}
// Два эквивалентных способа
fmt.Println((*p).X) // 3 — явное разыменование
fmt.Println(p.X) // 3 — автоматическое (удобнее)
// Изменение через указатель
p.X = 10
fmt.Println(p.X) // 10Когда метод структуры объявлен с pointer receiver (func (p *Point) Scale(f int)), он получает указатель и может изменять поля — это именно этот механизм.
Nil-указатель
Указатель, которому ничего не присвоили, равен nil. Разыменовать его нельзя — будет паника:
var p *int
fmt.Println(p) // <nil>
fmt.Println(p == nil) // true
// Паника: runtime error: invalid memory address or nil pointer dereference
// fmt.Println(*p)Правило простое: перед разыменованием проверяй nil, если указатель пришёл извне (аргумент функции, поле структуры):
func printValue(p *int) {
if p == nil {
fmt.Println("указатель nil")
return
}
fmt.Println(*p)
}Когда использовать указатели
| Ситуация | Рекомендация |
|---|---|
| Функция должна изменить аргумент | *T |
| Большая структура (копирование дорого) | *T |
| Маленькое значение (int, bool, small struct) | по значению |
| Нужно выразить «отсутствие значения» | *T (nil = нет) |
В отличие от C, в Go нет арифметики указателей — нельзя написать p++ или p + 2. Указатель либо указывает на конкретную переменную, либо nil. Это сознательное ограничение для безопасности.