Указатель является переменной, что указывает на адрес другой переменной. В программировании указатели являются формой косвенной адресации, что может быть довольно мощным инструментом.

После изучения данного урока вы сможете:

Форум Гоферов

Мы работаем над форумом для программистов на Golang. Очень нужны модераторы которые хотят помочь с ответами для новичков и помочь в развитии Go-сообщества.

Go на Форум

Уроки, статьи и Видео

Мы публикуем в паблике ВК и Telegram качественные обучающие материалы для быстрого изучения Go. Подпишитесь на нас в ВК и в Telegram. Поддержите сообщество Go программистов.

Go в ВК Go в Telegram

  • Объявлять и использовать указатели;
  • Разобраться в связи между указателями и памятью RAM;
  • Увидеть, когда указатели требуются, а когда нет.

Практически у каждого городского здания есть небольшая табличка, где указано название улицы и номер дома. Такая система помогает ориентироваться в местности и помогает избежать путаницы. На двери закрывшихся магазинов нередко приклеивают объявления с пояснениями вроде «Извините, мы переехали!» и новым адресом. Указатели в программировании напоминают записки подобного рода, что указывают на другой адрес.

Указатели в Golang

Содержание статьи

Все проблемы в программировании можно решить через очередной уровень косвенной адресации…

Дэвид Вилер

Указатели очень полезны, однако за ними сложилась не очень приятная репутация. Языки прошлого, С в частности, делают акцент на безопасности. Сбои и уязвимости в защите зачастую связаны с неправильным использованием указателей. Вследствие этого, стал заметен рост популярности тех языков, что не сильно полагаются на указатели.

В Go также есть указатели, и они учитывают вопросы безопасности. Go не заботят проблемы висячих указателей. Их можно сравнить со случаем, когда вы направляетесь в любимый магазин, однако, прибыв на место, внезапно видите парковку больницы.

Если вы раньше работали с указателями, не переживайте. Сейчас все будет намного лучше. Если вы сталкиваетесь с указателями впервые, расслабьтесь. Go станет отличной отправной точкой для изучения указателей.

Как и вывеска на дверях магазина, что указывает на новый адрес, указатели направляют компьютер к месту, где нужно искать запрашиваемое значение. Подумайте о других жизненных ситуациях, что похожи на работу указателей.

Амперсанд (&) и звездочка астериск (*) в Golang

Указатели в Go адаптируют хорошо-установившийся синтаксис, используемый С. Стоит обратить внимание на два символа — амперсанд (&) и звездочка астериск (*). Однако у звездочки два назначения, о которых вы узнаете далее.

Оператор адреса представлен амперсандом. Он определяет адрес переменной в памяти. Значения переменных хранятся в памяти RAM, а место хранения переменной называют адресом памяти. Следующий код выводит адрес памяти шестнадцатеричного числа. Обратите внимание, что адрес на вашем компьютере будет отличаться.

Это место в памяти, где компьютер хранит 42. К счастью, вы можете использовать название переменной answer для получения переменной вместо того адреса памяти, что использует компьютер.

На заметку: Вы не можете взять адрес строкового литерала, числа или булева значения. Компилятор Go сообщит об ошибке для &42 или &"another level of indirection".

Оператор адреса (&) предоставляет адрес памяти значения. Обратная операция называется разыменованием, что выдает значение, к которому обращается адрес памяти. В следующем листинге происходит разыменование переменная address разыменуется через префикс в виде астерикса (*).

В предыдущем коде и на Схеме 1 переменная address содержит адрес памяти для answer. Самого answer (42) там нет, однако известно, где его можно найти.

Адресами памяти в С можно манипулировать через арифметику указателей ( к примеру, address++), но Go не разрешает использовать небезопасные операции.

указатели golang

Схема 1: address указывает на answer

Вопросы для проверки:

  1. Что fmt.Println(*&answer) отображает в Листинге 2?
  2. Как компилятор Go может узнать разницу между разыменованием и умножением?

Типы указателей в Golang

Указатели хранят адреса памяти.

Переменная address в Листинге 2 является указателем типа *int, об этом сообщает специальный символ в следующем примере.

Звездочка в *int значит, что это тип указателя. В данном случае он может указать на другую переменную типа int.

Типы указателя могут появиться везде, где типы используются, включая объявления переменных, параметры функции, возвращаемые типы, типы полей структуры и так далее. В следующем примере звездочка (*) в объявлении home поясняет, что это тип указателя.

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

Переменная home в предыдущем листинге может указывать на любую переменную типа string Однако компилятор Go не позволяет home указывать на переменную любого другого типа, в том числе int.

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

Вопросы для проверки:

  1. Какой код вы бы использовали для объявления переменной под названием address, что может указывать на целые числа?
  2. В чем различие между объявлением типа указателя и разыменованием указателя в Листинге 4?

Указатели для указания Golang

Чарльз Болден стал администратором NASA в 17 июля 2009 года. Он принял пост после Кристофера Сколезе. Представляя должность администратора через указатель, следующий листинг может назначить administrator тому, кто выполняет данную роль. Для разъяснения обратите внимание на Схему 2.

указатели go

Схема 2: administrator указывает на bolden

Изменить значение bolden можно в одном месте, потому что переменная administrator указывает на bolden вместо хранения копии:

Можно разыменовать administrator для непрямого изменения значения bolden:

Результатом присваивания major к administrator является новый указатель, что также указывает на строку bolden. Подробнее на Схеме 3.

pointer go

Схема 3: administrator и major указывают на bolden

Указатели major и administrator оба содержат один и тот же адрес памяти, следовательно, они равны:

20 января 2017 года на смену Чарльзу Болдену пришел Роберт М. Лайтфут. После данного изменения administrator и major перестали указывать на одинаковый адрес памяти. Пояснение на Схеме 4.

указатели go

Схема 4: administrator теперь указывает на lightfoot

Присваивание разыменованного значения major к другой переменной создает копию строки. После создания клона прямые и непрямые изменения с bolden не будут иметь эффект над значением charles и наоборот:

Если две переменные содержат одинаковую строку, они считаются равными, как в случае с charles и bolden в следующем коде. Даже несмотря на то, что их адреса памяти отличаются:

В данном разделе значение bolden было изменено не напрямую через разыменование указателей administrator и major. Это демонстрирует то, что указатели могут сделать, что в данном случае будет прямым присваивание значений к bolden.

Вопросы для проверки:

  1. В чем преимущество использования указателя в Листинге 5?
  2. Опишите, что делают major := administrator и charles := *major.

Указатели и структуры Go

Указатели регулярно используются со структурами. По этой причине проектировщики языка Go решили ввести несколько удобных инструментов для указателей и структур.

В отличие от строк и чисел, перед композитными литералами ставится префикс в виде оператора адреса. В следующем примере переменная timmy содержит адрес памяти, указывающий на структуру person.

Кроме того, нет необходимости разыменования структур для получения доступа к их полю. Следующий листинг предпочтителен для написания (*timmy).superpower.

Вопросы для проверки:

1. Какие случаи использования оператора адреса действенны?

  • Строковые литералы: &"Timothy"
  • Целочисленные литералы: &10
  • Композитные литералы: &person{name: "Timothy"}
  • Все вышесказанные

2. В чем разница между timmy.superpower и (*timmy).superpower?

Указатели и массивы в Go

Как и в случае со структурами, композитные литералы для массивов могут дополнены префиксом в виде оператора адреса (&) для создания нового указателя на массив. Массивы также предоставляют автоматическое разыменование, как показано в следующем примере.

Массив из предыдущего примера автоматически разыменуется во время индексирования или создания среза. Нет необходимости писать более громоздкий (*superpowers)[0].

На заметку: В отличие от языка С, массивы и указатели в Go являются полностью независимыми типами.

Композитным литералам для срезов и карт также можно добавить префиксы с оператором адреса (&), однако тогда не будет автоматического разыменования.

Вопрос для проверки:

Назовите другой способ написания (*superpowers)[2:], где superpowers является указателем на массив.

Указатели в качестве параметров Golang

Указатели используются для возможности использования редактирования через границы методов и функций.

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

В Листинге 9 функция birthday объявляется с одним параметром типа *person. Это позволяет телу функции разыменовывать указатель и редактировать значение, на которое он указывает. Как и в Листинге 7, здесь не обязательно в открытую разыменовывать переменную p для получения доступа к полю age. Синтаксис следующего листинга предпочтительнее (*p).age++.

Функция birthday требует передачи указателя к person, как показано в следующем примере.

Вопросы для проверки:

1. Какой код вернет Timothy 11? Отталкивайтесь от Листинга 6.

  • birthday(&timmy)
  • birthday(timmy)
  • birthday(*timmy)

2. Сколько лет было бы Ребекке, если бы функция birthday(p person) не использовала указатель?

Приемники указателя в Golang

Приемники метода похожи на параметры. Метод birthday в следующем примере использует указатель для приемника, что позволяет методу изменять атрибуты персоны. Данное поведение похоже на функцию birthday в Листинге 9.

В следующем листинге при объявлении указателя и вызове метода birthday возраст Терри увеличивается.

Альтернативно, вызов метода в следующем листинге не использует указатель, однако по-прежнему работает. Go автоматически определяет адрес переменной (&) при вызове метода через пояснение с точкой, поэтому вам не нужно писать (&nathan).birthday().

Вызывается с указателем или нет, но метод birthday, объявленный в Листинге 11, должен уточнить приемник указателя — в противном случае age не увеличится.

Структуры регулярно передаются с указателями. Для метода birthday будет иметь смысл изменить атрибуты существующей person вместо создания новой персоны. Тем не менее, не каждую структуру стоит изменять. Стандартная библиотека предоставляет отличный пример в виде пакета time. Методы типа time.Time никогда не используют приемник указателя, предпочитая возвращать вместо этого новое время, как показано в следующем примере. В конечном итоге, завтра — это новое сегодня.

На заметку: Стоит использовать приемники указателей последовательно. Если некоторым методам нужны приемники указателей, используйте из для всех методов типа. Тема документации для более подробной информации.

Вопрос для проверки:

Как узнать, что time.Time никогда не использует приемник указателя?

Внутренние указатели Golang

В Go есть удобные внутренние указатели, которые нужны для определения адреса памяти поля внутри структуры. Функция levelUp в следующем примере изменяет структуру stats, и поэтому нуждается в указателе.

Оператор адреса в Go может использоваться для указания на поле внутри структуры, как показано в следующем примере.

У типа character нет указателей в определении структуры, однако в случае необходимости вы можете взять адрес памяти любого поля. Код &player.stats предоставляет указатель на внутреннюю часть структуры.

Вопрос для проверки:

Что из себя представляет внутренний указатель?

Изменение, или мутации массивов в Golang

Хотя предпочтительнее использовать срезы, а не массивы, в случаях, когда нет нужды менять их длину, можно задействовать и массивы. В качестве примера можно рассмотреть шахматную доску. В следующем примере показано, как указатели позволяют функциям изменять элементы массива.

В инструкции для создания Игры Жизнь используются срезы, хотя размер мира фиксированный. При задействовании указателей вы можете переписать код Игры Жизнь, включив массивы.

Вопрос для проверки: 

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

Не все мутации запрашивают открытого использования указателя. Для некоторых встроенных коллекций Go неявно задействует указатели.

Карты в роли указателей в Go

В уроке про карты мы говорили о том, что во время присваивания или передачи в качестве аргументов карты не копируются. Карты являются скрытыми указателями, поэтому указание на карту является лишним. Не делайте этого:

Это совершенно нормально, если ключ или значение карты является типом указателя, однако причины указания на карту встречаются чрезвычайно редко.

Вопрос для проверки:

Является ли карта указателем?

Срезы в Go — указатели на массив

Срезы являются своеобразными окнами в массив. Для указания на элемент массива срезы используют указатели.

Внутренне срез представлен как структура с тремя элементами: указатель на массив, вместимость среза и его длина. Внутренний указатель позволяет изменить базовые данные, когда срез передается напрямую функции или методу.

Явный указатель на срез полезен только в том случае, когда нужно модифицировать сам срез: длину, вместимость или начальный набор. В следующем примере функция reclassify редактирует длину среза planets. Вызов функции (main) не увидит изменения, если reclassify не использует указатель.

Вместо изменения переданного среза, как в Листинге 18, лучше написать функцию reclassify для возвращения нового среза.

Вопрос для проверки:

Для каким двух типов данных потребуется указатель в случае, когда функциям или методам нужно будет изменить полученные данные?

Указатели и интерфейсы в Golang

В следующем примере показано, что martian и указатель на martian удовлетворяют интерфейсу talker.

Все иначе, когда методы используют приемники указателя, как показано далее:

В предыдущем коде &pew принадлежит типу *laser, что удовлетворяет интерфейсу talker, что запрашивает shout. Однако shout(pew) не работает, так как laser в данном случае не удовлетворяет интерфейсу.

Вопрос для проверки:

Когда указатель удовлетворяет интерфейсу?

Правильное использование указателей в Golang

Указатели могут быть полезными, но они также добавляют сложность. Может быть сложнее понять код, где значения могут быть изменены из разных мест.

Используйте указатели, когда в этом есть смысл, не переусердствуйте. Языки программирования, что не сильно полагаются на указатели зачастую используют их неявно, к примеру, при создании класса нескольких объектов. С Go вы можете самостоятельно решить, когда задействовать указатели, а когда не стоит.

Вопрос для проверки:

Почему лучше не использовать указатели слишком часто?

Заключение

  • Указатели хранят адреса памяти;
  • Оператор адреса (&) предоставляет память адреса переменной;
  • Указатель может быть разыменован (*) для получения доступа или редактирования значения, на которое он указывает;
  • Указателями являются типы, объявленные со звездочкой-префиксом. К примеру, *int;
  • Используйте указатели для изменения значений через границы функций и методов;
  • Указатели наиболее полезны со структурами и массивами;
  • Карты и срезы неявно используют указатели;
  • Внутренние указатели могут указать на поля внутри структур без объявления данных полей как указателей;
  • Используйте указатели, когда в них есть смысл, но не переусердствуйте.

Итоговое задание для проверки:

Напишите программу с черепахой, которая может двигаться вверх, вниз, налево или направо. Черепаха должна сохранить координаты (x, y) места, где положительные значения для передвижения вниз и направо. Используйте методы для увеличения /уменьшения соответствующей переменной. Функция main должна использовать методы, которые вы написали, и выводить окончательное местоположение.

Обратите внимание, что приемники метода будут использовать указатели для манипулирования значениями x и y.