Все данные программы, включая переменные, хранятся в памяти компьютера. Нумерация поддерживает порядок в ячейках, и такие номера называются адресами. Каждая переменная имеет адрес в виде целого положительного числа.
Если нужно передать переменную из одной части программы в другую, хватит обычного копирования:
Значение переменной а
полностью скопировалось в b
.
В плане удобства это далеко не универсальный способ. Во-первых, размер переменной a
может быть очень большим, копирование займёт много времени. Во-вторых, иногда копии может быть недостаточно и нужно получить саму переменную — к примеру, чтобы изменить её значение.
Note
Тогда на помощь приходят указатели. Если вы знакомы с языком С, то легко разберётесь и с указателями в Go, в то время как программистам на Python придётся вникать в новую тему.
Итак, если у каждой переменной есть адрес, мы можем передавать его, а не сами данные, хранящиеся в ячейке памяти. За хранение значения адреса переменной в Go отвечают указатели. Данные любого типа хранятся в виде двоичных чисел, поэтому для определения конкретного типа данных в ячейке памяти указатель сам имеет соответствующий тип.
Синтаксис переменной типа «указатель» очень простой:
Здесь создали переменную типа «указатель на целое число». В Go можно создать указатель на любой тип данных.
Физически указатель — это ячейка памяти, хранящая адрес ячейки, на которую «смотрит» указатель. После создания указатель не «смотрит» ни на одну ячейку памяти в компьютере и имеет нулевое значение. Оно выглядит как nil
.
Для того чтобы присвоить указателю значение (адрес какой-либо переменной), используется операция взятия адреса &
:
Значение указателя на 64-битном компьютере — это 64-битное число. Именно размер указателя на данной системе задаёт характеристику битности компьютера.
На разных платформах значение указателя p
будет разным. Именно поэтому значение указателя не имеет смысла за пределами программы.
Чтобы получить значение указателя, в памяти должна быть переменная, на которую он «смотрит». Такое значение называется адресуемым (adressable). С константами сложнее — у них забрать адрес не получится.
Тип переменной, на которую создаётся указатель, должен соответствовать типу указателя.
Литералы композитных типов создают в памяти переменную соответствующего типа, поэтому указатель можно создать вот так:
А ещё в Go есть встроенная функция new()
. В качестве параметра ей передаётся тип, а возвращается указатель на новую переменную соответствующего типа.
Указатели ведут себя так же, как и обычные переменные. Их можно копировать, присваивая другим переменным тип указателя, передавать и возвращать в функции, а также создавать указатели уже на них.
Тип указателя на указатель описывается как **T
, например **int
.
Чтобы получить или изменить значение, хранящееся по указателю, применяют оператор разыменования (dereference) *
.
Вызов оператора разыменования на nil
-указателе приведёт к панике на этапе исполнения кода, и программа откажется работать дальше.
Указатели и структуры
Для указателей на структуры в Go есть возможность неявного разыменования при доступе к полям структуры.
Сравнение указателей
Для указателей определены операторы сравнения (==
, !=
). Два указателя равны, если они указывают на один и тот же объект в памяти либо если оба равны nil
.
Когда стоит использовать указатели
-
Когда нужно изменить значение переменной из вызываемой функции. Если передать переменную по значению, все модификации внутри функции применятся к локальной копии и оставят исходную переменную неизменной.
-
Когда нужно подчеркнуть, что значение может отсутствовать. Например, есть функция, которая возвращает запись о пользователе
type User struct{...}
по его идентификатору. Результат-указатель даёт понять, что не по всем идентификаторам может быть найден пользователь. Пример функции с такой сигнатурой: -
Когда вы работаете с ресурсами вроде файловых дескрипторов или сокетов. Копирование таких переменных может быть связано с исчерпанием системных ресурсов или вообще не производиться.
-
Когда вы работаете с большими переменными и на копирование по стеку затрачивается больше ресурсов, чем на сборку мусора от указателей.
Когда не стоит использовать указатели
- Когда хочется ускорить приложение и кажется, что копирование структур — слишком дорогая операция. До тех пор, пока нет тестов, однозначно показывающих, что указатели повышают производительность, лучше не пытаться оптимизировать. Вероятнее всего, напрасно потратите силы или снизите производительность системы, увеличив расходы на сборку мусора.
- Задумываться о замене передачи по значению на передачу по указателю стоит, когда размер структуры достигает порядка сотен байт.
- Когда множество указателей в памяти сильно нагружают сборщик мусора. Такое может произойти, к примеру, при создании собственной in-memoryбазы данных.
Сравнение указателей в Go и C/С++
Синтаксис указателей в Go идентичен побратимам из С, как и многие другие параметры. Однако есть пара важных различий.
Указатели в Go не имеют адресной арифметики. Несмотря на то что указатель хранит адрес, который является числом, к нему нельзя применять арифметические операции. Это не относится к недостаткам, потому что было сознательно убрано для повышения безопасности кода.
Указатель может «смотреть» не на любой участок памяти — только на существующий и соответствующий типу указателя.
По сути указатель близок к иммутабельности — его можно создать и присвоить адреса существующих переменных. В С это реализовано гораздо шире.
Сборщик мусора, одна из ключевых фишек Go, не сможет удалить переменную, пока на неё «смотрит» какой-либо указатель. Поэтому можно обойтись без ручного высвобождения памяти операцией free
.
Пример работы с указателями
Представим некоторую структуру, которая описывает пользователя:
В поле lastVisited
нужно сохранять дату последнего посещения. Если тип Person
используется в другом пакете, то напрямую lastVisited
изменить нельзя, ведь поле неэкспортируемое. Без указателей функция выглядела бы примерно так:
При таком подходе нужно не забывать присваивать переменной возвращаемое значение, так как в параметре передаётся копия объекта и все её изменения внутри функции не влияют на оригинальную переменную. Также, если тип аргумента имеет большой размер, то создание копии может занять больше времени, чем передача объекта по адресу.
C указателями всё становится проще:
Здесь в функцию был передан указатель на переменную, что позволило изменить её поле без дополнительного копирования. С другой стороны, если функция не изменяет передаваемые ей параметры, то лучше не использовать указатели, а передавать переменные по значению.
Указатели на bool
Допустим, в структуре есть поле типа *bool
и мы хотим, чтобы оно могло принимать значения nil
, true
, false
. Чтобы записать в него булевы значения, можно воспользоваться лайфхаком:
Здесь в анонимной функции создаётся переменная с нужным значением, и возвращается указатель на неё. Profit!
📂 Go | Последнее изменение: 15.11.2024 17:35