Указатели

Указатели — одна из тех тем, которая часто пугает новичков, но в Go они реализованы максимально логично и безопасно. В отличие от C++, здесь нет «арифметики указателей» (вы не можете прибавить единицу к адресу), что исключает целый класс ошибок с памятью.

По умолчанию Go передает аргументы в функции по значению. Это значит, что функция получает копию данных. Если вы измените копию, оригинал останется прежним.

Рассмотрим пример:

func zero(x int) {
    x = 0
}

func main() {
    x := 5
    zero(x)
    fmt.Println(x) // Выведет 5
}

Чтобы функция могла изменить оригинал, нам нужно передать ей не само значение, а адрес того места в памяти, где это значение лежит. Здесь на сцену выходят указатели.

Что такое указатель?

Указатель — это переменная, которая хранит адрес другой переменной. Вместо того чтобы нести «коробку с данными», указатель просто говорит: «данные лежат в коробке по адресу №123».

Операторы & и *

Для работы с указателями используются два символа:

  1. & (адрес): Возвращает адрес переменной.
  2. * (разыменование): Позволяет получить доступ к значению, лежащему по адресу.

Перепишем нашу программу:

func zero(xPtr *int) {
    *xPtr = 0 // Переходим по адресу и меняем значение на 0
}

func main() {
    x := 5
    zero(&x)       // Передаем адрес переменной x
    fmt.Println(x) // Теперь выведет 0
}

  • *int в сигнатуре функции — это тип «указатель на целое число».
  • *xPtr = 0 — это действие. Мы говорим: «Возьми адрес, хранящийся в xPtr, и запиши туда 0».

Оператор new

Еще один способ создать указатель — использовать встроенную функцию new. Она выделяет память под тип и возвращает указатель на «нулевое значение» этого типа.

xPtr := new(int) // Создает в памяти int (равный 0) и дает нам адрес
*xPtr = 10
fmt.Println(*xPtr) // 10

В Go вам не нужно вручную удалять объекты из памяти (как в C++). Сборщик мусора (Garbage Collector) автоматически очистит память, как только поймет, что на этот участок больше никто не ссылается.


Когда использовать указатели?

Как эксперт, дам совет: не используйте указатели везде подряд. Копирование маленьких переменных (как int или float64) происходит очень быстро.

Указатели нужны в двух случаях:

  1. Мутация: Когда функции необходимо изменить состояние передаваемого объекта.
  2. Эффективность: Когда вы передаете огромную структуру (например, массив на миллион элементов). Передать один адрес (8 байт) гораздо быстрее, чем копировать все данные.

Задачи

  • Адрес: С помощью какого оператора можно получить адрес существующей переменной?
  • Значение: С помощью какого оператора можно получить значение, на которое указывает указатель?
  • Квадрат: Какое значение будет у переменной после выполнения этого кода? ```go func square(x *float64) { *x = *x * *x } func main() { x := 1.5 square(&x) }


* [ ] **Обмен (Swap):** Напишите функцию `swap(x, y *int)`, которая меняет значения двух переменных местами.
* *Подсказка:* Вам понадобится временная переменная внутри функции.


```go
x := 1
y := 2
swap(&x, &y)
// Должно стать: x = 2, y = 1

Полезные ссылки

  1. Go by Example: Pointers — лаконичные примеры.
  2. A Tour of Go: Pointers — интерактивная практика.
  3. Language Mechanics On Pointers — глубокий разбор того, как указатели работают со стеком и кучей (на английском).