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

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

a := 5
 
var b int
b = a 

Значение переменной а полностью скопировалось в b.

В плане удобства это далеко не универсальный способ. Во-первых, размер переменной a может быть очень большим, копирование займёт много времени. Во-вторых, иногда копии может быть недостаточно и нужно получить саму переменную — к примеру, чтобы изменить её значение.

Note

Тогда на помощь приходят указатели. Если вы знакомы с языком С, то легко разберётесь и с указателями в Go, в то время как программистам на Python придётся вникать в новую тему.

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

Синтаксис переменной типа «указатель» очень простой:

var p *int 

Здесь создали переменную типа «указатель на целое число». В Go можно создать указатель на любой тип данных.

Физически указатель — это ячейка памяти, хранящая адрес ячейки, на которую «смотрит» указатель. После создания указатель не «смотрит» ни на одну ячейку памяти в компьютере и имеет нулевое значение. Оно выглядит как nil.

Для того чтобы присвоить указателю значение (адрес какой-либо переменной), используется операция взятия адреса &:

var a int = 5
p := &a
 
fmt.Println(a,p) //a=5 p=0xc0000b2008 

Значение указателя на 64-битном компьютере — это 64-битное число. Именно размер указателя на данной системе задаёт характеристику битности компьютера.

На разных платформах значение указателя p будет разным. Именно поэтому значение указателя не имеет смысла за пределами программы.

Чтобы получить значение указателя, в памяти должна быть переменная, на которую он «смотрит». Такое значение называется адресуемым (adressable). С константами сложнее — у них забрать адрес не получится.

  const c = 5
  p1 := &"abc" // ошибка компиляции
  p2 := &с // ошибка компиляции 

Тип переменной, на которую создаётся указатель, должен соответствовать типу указателя.

  var p *int
  var a int = 5
  var b string = "abc"
  p = &a 
  p = &b // ошибка компиляции 

Литералы композитных типов создают в памяти переменную соответствующего типа, поэтому указатель можно создать вот так:

  type A struct {
      IntField int
  }
  // Литерал А{} создаёт в памяти переменную типа А. Затем от неё берётся указатель
  p := &A{ 
      IntField: 10,
  } 

А ещё в Go есть встроенная функция new(). В качестве параметра ей передаётся тип, а возвращается указатель на новую переменную соответствующего типа.

    type A struct {
        IntField int
    }
    
    p := new(A) //  то же самое, что и &A{} 

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

Тип указателя на указатель описывается как **T, например **int.

Чтобы получить или изменить значение, хранящееся по указателю, применяют оператор разыменования (dereference) *.

i := 42
p := &i
fmt.Println(*p) // читаем значение переменной i через указатель p
*p = 21         // записываем в переменную i значение 21 через указатель p 

Вызов оператора разыменования на nil-указателе приведёт к панике на этапе исполнения кода, и программа откажется работать дальше.

var p *int
fmt.Println(*p) // panic: runtime error: invalid memory address or nil pointer dereference 

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

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

type A struct {
    IntField int
}
 
p := &A{}
p.IntField = 42 // вместо (*p).IntField = 42 

Сравнение указателей

Для указателей определены операторы сравнения (==, !=). Два указателя равны, если они указывают на один и тот же объект в памяти либо если оба равны nil.

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

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

      incrementCopy := func(i int) {
          i++
      }
     
      increment := func(i *int) {
          (*i)++
      }
     
      i := 42
     
      incrementCopy(i)
      fmt.Println(i) // 42
     
      increment(&i)
      fmt.Println(i) // 43
       
  • Когда нужно подчеркнуть, что значение может отсутствовать. Например, есть функция, которая возвращает запись о пользователе type User struct{...} по его идентификатору. Результат-указатель даёт понять, что не по всем идентификаторам может быть найден пользователь. Пример функции с такой сигнатурой:

      func FindUser(id int) *User
  • Когда вы работаете с ресурсами вроде файловых дескрипторов или сокетов. Копирование таких переменных может быть связано с исчерпанием системных ресурсов или вообще не производиться.

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

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

  • Когда хочется ускорить приложение и кажется, что копирование структур — слишком дорогая операция. До тех пор, пока нет тестов, однозначно показывающих, что указатели повышают производительность, лучше не пытаться оптимизировать. Вероятнее всего, напрасно потратите силы или снизите производительность системы, увеличив расходы на сборку мусора.
  • Задумываться о замене передачи по значению на передачу по указателю стоит, когда размер структуры достигает порядка сотен байт.
  • Когда множество указателей в памяти сильно нагружают сборщик мусора. Такое может произойти, к примеру, при создании собственной in-memoryбазы данных.

Сравнение указателей в Go и C/С++

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

Указатели в Go не имеют адресной арифметики. Несмотря на то что указатель хранит адрес, который является числом, к нему нельзя применять арифметические операции. Это не относится к недостаткам, потому что было сознательно убрано для повышения безопасности кода.

Указатель может «смотреть» не на любой участок памяти — только на существующий и соответствующий типу указателя.

По сути указатель близок к иммутабельности — его можно создать и присвоить адреса существующих переменных. В С это реализовано гораздо шире.

Сборщик мусора, одна из ключевых фишек Go, не сможет удалить переменную, пока на неё «смотрит» какой-либо указатель. Поэтому можно обойтись без ручного высвобождения памяти операцией free.

Пример работы с указателями

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

type Person struct {
  Name string
  Age int
  lastVisited time.Time
}  

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

func GetPersonWithLastVisited(p Person) Person {
  p.lastVisited = time.Now() // time.Now() возвращает текущее время
    return p
}
 
// использование в другом пакете
p := Person{
  Name: "Alex",
  Age: 25,
}
p = GetPersonWithLastVisited(p) 

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

C указателями всё становится проще:

func UpdatePersonWithLastVisited (p *Person )  {
  p.lastVisited = time.Now() 
} 
 
// использование в другом пакете
p := Person{
  Name: "Alex",
  Age: 25,
}
UpdatePersonWithLastVisited(&p) 

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

Указатели на bool

Допустим, в структуре есть поле типа *bool и мы хотим, чтобы оно могло принимать значения nil, true, false. Чтобы записать в него булевы значения, можно воспользоваться лайфхаком:

filters.IsActive = func() *bool { b := true; return &b }()

Здесь в анонимной функции создаётся переменная с нужным значением, и возвращается указатель на неё. Profit!


📂 Go | Последнее изменение: 15.11.2024 17:35