Указатель является переменной, что указывает на адрес другой переменной. В программировании указатели являются формой косвенной адресации, что может быть довольно мощным инструментом.
После изучения данного урока вы сможете:
Рекомендуем вам супер TELEGRAM канал по Golang где собраны все материалы для качественного изучения языка. Удивите всех своими знаниями на собеседовании! 😎
Мы публикуем в паблике ВК и Telegram качественные обучающие материалы для быстрого изучения Go. Подпишитесь на нас в ВК и в Telegram. Поддержите сообщество Go программистов.
- Объявлять и использовать указатели;
- Разобраться в связи между указателями и памятью RAM;
- Увидеть, когда указатели требуются, а когда нет.
Практически у каждого городского здания есть небольшая табличка, где указано название улицы и номер дома. Такая система помогает ориентироваться в местности и помогает избежать путаницы. На двери закрывшихся магазинов нередко приклеивают объявления с пояснениями вроде «Извините, мы переехали!» и новым адресом. Указатели в программировании напоминают записки подобного рода, что указывают на другой адрес.
Содержание статьи
- Амперсанд (&) и звездочка астериск (*) в Golang
- Типы указателей в Golang
- Указатели для указания
- Указатели и структуры Go
- Указатели и массивы
- Указатели в качестве параметров Golang
- Приемники указателя
- Внутренние указатели Golang
- Изменение, или мутации массивов
- Скрытые указатели в Golang
- Карты в роли указателей
- Срезы Go — указатели на массив
- Указатели и интерфейсы
- Правильное использование указателей
Все проблемы в программировании можно решить через очередной уровень косвенной адресации…
Дэвид Вилер
Указатели очень полезны, однако за ними сложилась не очень приятная репутация. Языки прошлого, С в частности, делают акцент на безопасности. Сбои и уязвимости в защите зачастую связаны с неправильным использованием указателей. Вследствие этого, стал заметен рост популярности тех языков, что не сильно полагаются на указатели.
В Go также есть указатели, и они учитывают вопросы безопасности. Go не заботят проблемы висячих указателей. Их можно сравнить со случаем, когда вы направляетесь в любимый магазин, однако, прибыв на место, внезапно видите парковку больницы.
Если вы раньше работали с указателями, не переживайте. Сейчас все будет намного лучше. Если вы сталкиваетесь с указателями впервые, расслабьтесь. Go станет отличной отправной точкой для изучения указателей.
Как и вывеска на дверях магазина, что указывает на новый адрес, указатели направляют компьютер к месту, где нужно искать запрашиваемое значение. Подумайте о других жизненных ситуациях, что похожи на работу указателей.
Амперсанд (&) и звездочка астериск (*) в Golang
Указатели в Go адаптируют хорошо-установившийся синтаксис, используемый С. Стоит обратить внимание на два символа — амперсанд (&
) и звездочка астериск (*
). Однако у звездочки два назначения, о которых вы узнаете далее.
Оператор адреса представлен амперсандом. Он определяет адрес переменной в памяти. Значения переменных хранятся в памяти RAM, а место хранения переменной называют адресом памяти. Следующий код выводит адрес памяти шестнадцатеричного числа. Обратите внимание, что адрес на вашем компьютере будет отличаться.
1 2 |
answer := 42 fmt.Println(&answer) // Выводит: 0x1040c108 |
Это место в памяти, где компьютер хранит 42. К счастью, вы можете использовать название переменной answer
для получения переменной вместо того адреса памяти, что использует компьютер.
На заметку: Вы не можете взять адрес строкового литерала, числа или булева значения. Компилятор Go сообщит об ошибке для
&42
или&"another level of indirection"
.
Оператор адреса (&) предоставляет адрес памяти значения. Обратная операция называется разыменованием, что выдает значение, к которому обращается адрес памяти. В следующем листинге происходит разыменование переменная address
разыменуется через префикс в виде астерикса (*
).
1 2 3 4 5 |
answer := 42 fmt.Println(&answer) // Выводит: 0x1040c108 address := &answer fmt.Println(*address) // Выводит: 42 |
В предыдущем коде и на Схеме 1 переменная address
содержит адрес памяти для answer
. Самого answer
(42) там нет, однако известно, где его можно найти.
Адресами памяти в С можно манипулировать через арифметику указателей ( к примеру, address++
), но Go не разрешает использовать небезопасные операции.
Схема 1: address
указывает на answer
Вопросы для проверки:
- Что
fmt.Println(*&answer)
отображает в Листинге 2? - Как компилятор Go может узнать разницу между разыменованием и умножением?
Типы указателей в Golang
Указатели хранят адреса памяти.
Переменная address
в Листинге 2 является указателем типа *int
, об этом сообщает специальный символ %Т
в следующем примере.
1 2 3 4 |
answer := 42 address := &answer fmt.Printf("address это %T\n", address) // Выводит: address это *int |
Звездочка в *int
значит, что это тип указателя. В данном случае он может указать на другую переменную типа int
.
Типы указателя могут появиться везде, где типы используются, включая объявления переменных, параметры функции, возвращаемые типы, типы полей структуры и так далее. В следующем примере звездочка (*) в объявлении home
поясняет, что это тип указателя.
1 2 3 4 5 6 7 |
canada := "Canada" var home *string fmt.Printf("home is a %T\n", home) // Выводит: home is a *string home = &canada fmt.Println(*home) // Выводит: Canada |
Звездочка перед типом обозначает тип указателя, а звездочка перед названием переменной нужна для указания на значение, к которому отсылается указатель.
Переменная home
в предыдущем листинге может указывать на любую переменную типа string
Однако компилятор Go не позволяет home
указывать на переменную любого другого типа, в том числе int
.
Тип системы С легко убедить в том, что адрес памяти содержит другой тип. Временами это может быть полезно, однако Go избегает потенциально небезопасных операций.
Вопросы для проверки:
- Какой код вы бы использовали для объявления переменной под названием
address
, что может указывать на целые числа? - В чем различие между объявлением типа указателя и разыменованием указателя в Листинге 4?
Указатели для указания Golang
Чарльз Болден стал администратором NASA в 17 июля 2009 года. Он принял пост после Кристофера Сколезе. Представляя должность администратора через указатель, следующий листинг может назначить administrator
тому, кто выполняет данную роль. Для разъяснения обратите внимание на Схему 2.
1 2 3 4 5 6 7 8 9 |
var administrator *string scolese := "Christopher J. Scolese" administrator = &scolese fmt.Println(*administrator) // Выводит: Christopher J. Scolese bolden := "Charles F. Bolden" administrator = &bolden fmt.Println(*administrator) // Выводит: Charles F. Bolden |
Схема 2: administrator
указывает на bolden
Изменить значение bolden
можно в одном месте, потому что переменная administrator
указывает на bolden
вместо хранения копии:
1 2 |
bolden = "Charles Frank Bolden Jr." fmt.Println(*administrator) // Выводит: Charles Frank Bolden Jr. |
Можно разыменовать administrator
для непрямого изменения значения bolden
:
1 2 |
*administrator = "Maj. Gen. Charles Frank Bolden Jr." fmt.Println(bolden) // Выводит: Maj. Gen. Charles Frank Bolden Jr. |
Результатом присваивания major
к administrator
является новый указатель, что также указывает на строку bolden
. Подробнее на Схеме 3.
1 2 3 |
major := administrator *major = "Major General Charles Frank Bolden Jr." fmt.Println(bolden) // Выводит: Major General Charles Frank Bolden Jr. |
Схема 3: administrator
и major
указывают на bolden
Указатели major
и administrator
оба содержат один и тот же адрес памяти, следовательно, они равны:
1 |
fmt.Println(administrator == major) // Выводит: true |
20 января 2017 года на смену Чарльзу Болдену пришел Роберт М. Лайтфут. После данного изменения administrator
и major
перестали указывать на одинаковый адрес памяти. Пояснение на Схеме 4.
1 2 3 |
lightfoot := "Robert M. Lightfoot Jr." administrator = &lightfoot fmt.Println(administrator == major) // Выводит: false |
Схема 4: administrator
теперь указывает на lightfoot
Присваивание разыменованного значения major
к другой переменной создает копию строки. После создания клона прямые и непрямые изменения с bolden
не будут иметь эффект над значением charles
и наоборот:
1 2 3 4 |
charles := *major *major = "Charles Bolden" fmt.Println(charles) // Выводит: Major General Charles Frank Bolden Jr. fmt.Println(bolden) // Выводит: Charles Bolden |
Если две переменные содержат одинаковую строку, они считаются равными, как в случае с charles
и bolden
в следующем коде. Даже несмотря на то, что их адреса памяти отличаются:
1 2 3 |
charles = "Charles Bolden" fmt.Println(charles == bolden) // Выводит: true fmt.Println(&charles == &bolden) // Выводит: false |
В данном разделе значение bolden
было изменено не напрямую через разыменование указателей administrator
и major
. Это демонстрирует то, что указатели могут сделать, что в данном случае будет прямым присваивание значений к bolden
.
Вопросы для проверки:
- В чем преимущество использования указателя в Листинге 5?
- Опишите, что делают
major := administrator
иcharles := *major
.
Указатели и структуры Go
Указатели регулярно используются со структурами. По этой причине проектировщики языка Go решили ввести несколько удобных инструментов для указателей и структур.
В отличие от строк и чисел, перед композитными литералами ставится префикс в виде оператора адреса. В следующем примере переменная timmy
содержит адрес памяти, указывающий на структуру person
.
1 2 3 4 5 6 7 8 9 |
type person struct { name, superpower string age int } timmy := &person{ name: "Timothy", age: 10, } |
Кроме того, нет необходимости разыменования структур для получения доступа к их полю. Следующий листинг предпочтителен для написания (*timmy).superpower
.
1 2 |
timmy.superpower = "flying" fmt.Printf("%+v\n", timmy) // Выводит: &{name:Timothy superpower:flying age:10} |
Вопросы для проверки:
1. Какие случаи использования оператора адреса действенны?
- Строковые литералы:
&"Timothy"
- Целочисленные литералы:
&10
- Композитные литералы:
&person{name: "Timothy"}
- Все вышесказанные
2. В чем разница между timmy.superpower
и (*timmy).superpower
?
Указатели и массивы в Go
Как и в случае со структурами, композитные литералы для массивов могут дополнены префиксом в виде оператора адреса (&) для создания нового указателя на массив. Массивы также предоставляют автоматическое разыменование, как показано в следующем примере.
1 2 3 |
superpowers := &[3]string{"flight", "invisibility", "super strength"} fmt.Println(superpowers[0]) // Выводит: flight fmt.Println(superpowers[1:2]) // Выводит: [invisibility] |
Массив из предыдущего примера автоматически разыменуется во время индексирования или создания среза. Нет необходимости писать более громоздкий (*superpowers)[0]
.
На заметку: В отличие от языка С, массивы и указатели в Go являются полностью независимыми типами.
Композитным литералам для срезов и карт также можно добавить префиксы с оператором адреса (&), однако тогда не будет автоматического разыменования.
Вопрос для проверки:
Назовите другой способ написания (*superpowers)[2:]
, где superpowers
является указателем на массив.
Указатели в качестве параметров Golang
Указатели используются для возможности использования редактирования через границы методов и функций.
Параметры функции и метода передаются через значение. Это значит, что функции всегда оперируют копией переданных аргументов. Когда указатель передается функции, функция получает копию адреса памяти. Через разыменование адреса памяти функция может изменить значение, на которое указывает указатель.
В Листинге 9 функция birthday
объявляется с одним параметром типа *person
. Это позволяет телу функции разыменовывать указатель и редактировать значение, на которое он указывает. Как и в Листинге 7, здесь не обязательно в открытую разыменовывать переменную p
для получения доступа к полю age
. Синтаксис следующего листинга предпочтительнее (*p).age++
.
1 2 3 4 5 6 7 8 |
type person struct { name, superpower string age int } func birthday(p *person) { p.age++ } |
Функция birthday
требует передачи указателя к person
, как показано в следующем примере.
1 2 3 4 5 6 7 8 9 |
rebecca := person{ name: "Rebecca", superpower: "imagination", age: 14, } birthday(&rebecca) fmt.Printf("%+v\n", rebecca) // Выводит: {name:Rebecca superpower:imagination age:15} |
Вопросы для проверки:
1. Какой код вернет Timothy 11
? Отталкивайтесь от Листинга 6.
- birthday(&timmy)
- birthday(timmy)
- birthday(*timmy)
2. Сколько лет было бы Ребекке, если бы функция birthday(p person)
не использовала указатель?
Приемники указателя в Golang
Приемники метода похожи на параметры. Метод birthday
в следующем примере использует указатель для приемника, что позволяет методу изменять атрибуты персоны. Данное поведение похоже на функцию birthday
в Листинге 9.
1 2 3 4 5 6 7 8 |
type person struct { name string age int } func (p *person) birthday() { p.age++ } |
В следующем листинге при объявлении указателя и вызове метода birthday
возраст Терри увеличивается.
1 2 3 4 5 6 |
terry := &person{ name: "Terry", age: 15, } terry.birthday() fmt.Printf("%+v\n", terry) // Выводит: &{name:Terry age:16} |
Альтернативно, вызов метода в следующем листинге не использует указатель, однако по-прежнему работает. Go автоматически определяет адрес переменной (&) при вызове метода через пояснение с точкой, поэтому вам не нужно писать (&nathan).birthday()
.
1 2 3 4 5 6 |
nathan := person{ name: "Nathan", age: 17, } nathan.birthday() fmt.Printf("%+v\n", nathan) // Выводит: {name:Nathan age:18} |
Вызывается с указателем или нет, но метод birthday
, объявленный в Листинге 11, должен уточнить приемник указателя — в противном случае age
не увеличится.
Структуры регулярно передаются с указателями. Для метода birthday
будет иметь смысл изменить атрибуты существующей person
вместо создания новой персоны. Тем не менее, не каждую структуру стоит изменять. Стандартная библиотека предоставляет отличный пример в виде пакета time
. Методы типа time.Time
никогда не используют приемник указателя, предпочитая возвращать вместо этого новое время, как показано в следующем примере. В конечном итоге, завтра — это новое сегодня.
1 2 3 4 5 |
const layout = "Mon, Jan 2, 2006" day := time.Now() tomorrow := day.Add(24 * time.Hour) fmt.Println(day.Format(layout)) // Выводит: Tue, Nov 10, 2009 fmt.Println(tomorrow.Format(layout)) // Выводит: Wed, Nov 11, 2009 |
На заметку: Стоит использовать приемники указателей последовательно. Если некоторым методам нужны приемники указателей, используйте из для всех методов типа. Тема документации для более подробной информации.
Вопрос для проверки:
Как узнать, что time.Time
никогда не использует приемник указателя?
Внутренние указатели Golang
В Go есть удобные внутренние указатели, которые нужны для определения адреса памяти поля внутри структуры. Функция levelUp
в следующем примере изменяет структуру stats
, и поэтому нуждается в указателе.
1 2 3 4 5 6 7 8 9 10 |
type stats struct { level int endurance, health int } func levelUp(s *stats) { s.level++ s.endurance = 42 + (14 * s.level) s.health = 5 * s.endurance } |
Оператор адреса в Go может использоваться для указания на поле внутри структуры, как показано в следующем примере.
1 2 3 4 5 6 7 8 9 |
type character struct { name string stats stats } player := character{name: "Matthias"} levelUp(&player.stats) fmt.Printf("%+v\n", player.stats) // Выводит: {level:1 endurance:56 health:280} |
У типа character
нет указателей в определении структуры, однако в случае необходимости вы можете взять адрес памяти любого поля. Код &player.stats
предоставляет указатель на внутреннюю часть структуры.
Вопрос для проверки:
Что из себя представляет внутренний указатель?
Изменение, или мутации массивов в Golang
Хотя предпочтительнее использовать срезы, а не массивы, в случаях, когда нет нужды менять их длину, можно задействовать и массивы. В качестве примера можно рассмотреть шахматную доску. В следующем примере показано, как указатели позволяют функциям изменять элементы массива.
1 2 3 4 5 6 7 8 9 10 11 |
func reset(board *[8][8]rune) { board[0][0] = 'r' // ... } func main() { var board [8][8]rune reset(&board) fmt.Printf("%c", board[0][0]) // Выводит: r } |
В инструкции для создания Игры Жизнь используются срезы, хотя размер мира фиксированный. При задействовании указателей вы можете переписать код Игры Жизнь, включив массивы.
Вопрос для проверки:
Когда лучше использовать указатель на массив?
Скрытые указатели в Golang
Не все мутации запрашивают открытого использования указателя. Для некоторых встроенных коллекций Go неявно задействует указатели.
Карты в роли указателей в Go
В уроке про карты мы говорили о том, что во время присваивания или передачи в качестве аргументов карты не копируются. Карты являются скрытыми указателями, поэтому указание на карту является лишним. Не делайте этого:
1 |
func demolish(planets *map[string]string) // Лишний указатель |
Это совершенно нормально, если ключ или значение карты является типом указателя, однако причины указания на карту встречаются чрезвычайно редко.
Вопрос для проверки:
Является ли карта указателем?
Срезы в Go — указатели на массив
Срезы являются своеобразными окнами в массив. Для указания на элемент массива срезы используют указатели.
Внутренне срез представлен как структура с тремя элементами: указатель на массив, вместимость среза и его длина. Внутренний указатель позволяет изменить базовые данные, когда срез передается напрямую функции или методу.
Явный указатель на срез полезен только в том случае, когда нужно модифицировать сам срез: длину, вместимость или начальный набор. В следующем примере функция reclassify
редактирует длину среза planets
. Вызов функции (main
) не увидит изменения, если reclassify
не использует указатель.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
func reclassify(planets *[]string) { *planets = (*planets)[0:8] } func main() { planets := []string{ "Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto", } reclassify(&planets) fmt.Println(planets) // Выводит: [Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune] } |
Вместо изменения переданного среза, как в Листинге 18, лучше написать функцию reclassify
для возвращения нового среза.
Вопрос для проверки:
Для каким двух типов данных потребуется указатель в случае, когда функциям или методам нужно будет изменить полученные данные?
Указатели и интерфейсы в Golang
В следующем примере показано, что martian
и указатель на martian
удовлетворяют интерфейсу talker
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
type talker interface { talk() string } func shout(t talker) { louder := strings.ToUpper(t.talk()) fmt.Println(louder) } type martian struct{} func (m martian) talk() string { return "nack nack" } func main() { shout(martian{}) // Выводит: NACK NACK shout(&martian{}) // Выводит: NACK NACK |
Все иначе, когда методы используют приемники указателя, как показано далее:
1 2 3 4 5 6 7 8 9 10 |
type laser int func (l *laser) talk() string { return strings.Repeat("pew ", int(*l)) } func main() { pew := laser(2) shout(&pew) Выводит: PEW PEW } |
В предыдущем коде &pew
принадлежит типу *laser
, что удовлетворяет интерфейсу talker
, что запрашивает shout
. Однако shout(pew)
не работает, так как laser
в данном случае не удовлетворяет интерфейсу.
Вопрос для проверки:
Когда указатель удовлетворяет интерфейсу?
Правильное использование указателей в Golang
Указатели могут быть полезными, но они также добавляют сложность. Может быть сложнее понять код, где значения могут быть изменены из разных мест.
Используйте указатели, когда в этом есть смысл, не переусердствуйте. Языки программирования, что не сильно полагаются на указатели зачастую используют их неявно, к примеру, при создании класса нескольких объектов. С Go вы можете самостоятельно решить, когда задействовать указатели, а когда не стоит.
Вопрос для проверки:
Почему лучше не использовать указатели слишком часто?
Заключение
- Указатели хранят адреса памяти;
- Оператор адреса (
&
) предоставляет память адреса переменной; - Указатель может быть разыменован (
*
) для получения доступа или редактирования значения, на которое он указывает; - Указателями являются типы, объявленные со звездочкой-префиксом. К примеру,
*int
; - Используйте указатели для изменения значений через границы функций и методов;
- Указатели наиболее полезны со структурами и массивами;
- Карты и срезы неявно используют указатели;
- Внутренние указатели могут указать на поля внутри структур без объявления данных полей как указателей;
- Используйте указатели, когда в них есть смысл, но не переусердствуйте.
Итоговое задание для проверки:
Напишите программу с черепахой, которая может двигаться вверх, вниз, налево или направо. Черепаха должна сохранить координаты (x, y) места, где положительные значения для передвижения вниз и направо. Используйте методы для увеличения /уменьшения соответствующей переменной. Функция main
должна использовать методы, которые вы написали, и выводить окончательное местоположение.
Обратите внимание, что приемники метода будут использовать указатели для манипулирования значениями x и y.
Администрирую данный сайт с целью распространения как можно большего объема обучающего материала для языка программирования Go. В IT с 2008 года, с тех пор изучаю и применяю интересующие меня технологии. Проявляю огромный интерес к машинному обучению и анализу данных.
E-mail: vasile.buldumac@ati.utm.md
Образование
Технический Университет Молдовы (utm.md), Факультет Вычислительной Техники, Информатики и Микроэлектроники
- 2014 — 2018 Universitatea Tehnică a Moldovei, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
- 2018 — 2020 Universitatea Tehnică a Moldovei, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»