Указатели в Go

Введение

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

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

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

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

При использовании указателя на переменную нужно понять два разных элемента синтаксиса. Первый элемент называется амперсандом (&). Если вы ставите амперсанд перед именем переменной, вы указываете, что хотите получить адрес или указатель для этой переменной. Второй элемент синтаксиса — звездочка (*) или оператор разыменовывания. При декларировании переменной указателя необходимо обеспечить соответствие имени переменной типу переменной, на которую указывает указатель, с префиксом *, примерно так:

var myPointer *int32 = &someint 

При этом создается указатель myPointer на переменную int32, который инициализирует указатель с адресом someint. Указатель не содержит переменную int32, а содержит только ее адрес.

Давайте рассмотрим указатель на строку. Следующий код декларирует значение строки и указатель на строку:

main.go

package main  import "fmt"  func main() {     var creature string = "shark"     var pointer *string = &creature      fmt.Println("creature =", creature)     fmt.Println("pointer =", pointer) }  

Запустите программу с помощью следующей команды:

  • go run main.go

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

Outputcreature = shark pointer = 0xc0000721e0 

Мы присвоим первой определяемой переменной имя creature и зададим ее равной строке со значением shark. Затем мы создадим другую переменную с именем pointer. Теперь мы зададим в качестве значения переменной pointer адрес переменной creature. Мы сохраним адрес значения в переменной, используя символ амперсанда (&). Это означает, что переменная pointer хранит адрес переменной creature, а не ее реальное значение.

Поэтому, когда мы вывели значение pointer, мы получили значение 0xc0000721e0, которое представляет собой адрес хранения переменной creature в компьютерной памяти.

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

main.go

 package main  import "fmt"  func main() {     var creature string = "shark"     var pointer *string = &creature      fmt.Println("creature =", creature)     fmt.Println("pointer =", pointer)      fmt.Println("*pointer =", *pointer) } 

Если вы выполните этот код, вы увидите следующие результаты:

Outputcreature = shark pointer = 0xc000010200 *pointer = shark 

Последняя добавленная нами строка убирает ссылку на переменную pointer и выводит значение, сохраненное по этому адресу.

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

main.go

package main  import "fmt"  func main() {     var creature string = "shark"     var pointer *string = &creature      fmt.Println("creature =", creature)     fmt.Println("pointer =", pointer)      fmt.Println("*pointer =", *pointer)      *pointer = "jellyfish"     fmt.Println("*pointer =", *pointer) }  

Запустите этот код, чтобы увидеть результаты:

Outputcreature = shark pointer = 0xc000094040 *pointer = shark *pointer = jellyfish 

Мы зададим значение, на которое ссылается переменная pointer, добавив звездочку (*) перед именем переменной, а затем зададим новое значение jellyfish. Как видите, при выводе значения разыменованной ссылки, сейчас она задана как jellyfish.

Возможно вы не поняли этого, но мы фактически изменили значение переменной creature. Это связано с тем, что переменная pointer фактически указывает на адрес переменной creature. Это означает, что если мы изменим значение, на которое указывает переменная pointer, мы также изменим значение переменной creature.

main.go

package main  import "fmt"  func main() {     var creature string = "shark"     var pointer *string = &creature      fmt.Println("creature =", creature)     fmt.Println("pointer =", pointer)      fmt.Println("*pointer =", *pointer)      *pointer = "jellyfish"     fmt.Println("*pointer =", *pointer)      fmt.Println("creature =", creature) } 

Результат выглядит следующим образом:

Outputcreature = shark pointer = 0xc000010200 *pointer = shark *pointer = jellyfish creature = jellyfish 

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

Помите, что мы выводим значение pointer, чтобы проиллюстрировать, что это указатель. На практике значение указателя используется только для получения или обновления этого значения.

Приемники указателя функции

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

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

Чтобы увидеть разницу, вначале рассмотрим функцию, передающую аргумент по значению:

main.go

package main  import "fmt"  type Creature struct {     Species string }  func main() {     var creature Creature = Creature{Species: "shark"}      fmt.Printf("1) %+vn", creature)     changeCreature(creature)     fmt.Printf("3) %+vn", creature) }  func changeCreature(creature Creature) {     creature.Species = "jellyfish"     fmt.Printf("2) %+vn", creature) }  

Результат выглядит следующим образом:

Output1) {Species:shark} 2) {Species:jellyfish} 3) {Species:shark} 

Вначале мы создали пользовательский тип с именем Creature. Он содержит одно поле с именем Species, которое представляет собой строку. В функции main мы создали экземпляр нового типа с именем creature и задали для поля Species значение shark. Затем мы вывели переменную для отображения текущего значения, сохраненного в переменной creature.

Далее мы вызвали changeCreature и передали копию переменной creature.

Функция changeCreature принимает один аргумент с именем creature, который относится к ранее определенному типу Creature. Затем мы изменяем значение поля Species на jellyfish и выводим его. Обратите внимание, что в функции changeCreature значение Species теперь jellyfish, и функция выводит 2) {Species:jellyfish}. Это связано с тем, что нам разрешено изменять значение в составе функции.

Однако когда последняя строка функции main распечатывает значение creature, значение Species сохраняется как shark. Значение осталось без изменений, потому что мы передали переменную по_ значению_. Это означает, что копия данного значения была создана в памяти и передана в функцию changeCreature. Это позволяет нам иметь функцию, которая сможет изменять любые передаваемые в нее аргументы, но не сможет влиять на переменные за пределами функции.

Затем измените функцию changeCreature так, чтобы она принимала аргумент по ссылке. Для этого мы можем изменить тип creature на указатель, используя оператор звездочка (*). Вместо передачи creature мы передаем указатель на creature или *creature. В предыдущем примере creature представляет собой структуру, где Species имеет значение shark. *creature является указателем, а не структурой, и его значение является адресом в памяти, и именно это мы передаем в функцию changeCreature().

main.go

package main  import "fmt"  type Creature struct {     Species string }  func main() {     var creature Creature = Creature{Species: "shark"}      fmt.Printf("1) %+vn", creature)     changeCreature(&creature)     fmt.Printf("3) %+vn", creature) }  func changeCreature(creature *Creature) {     creature.Species = "jellyfish"     fmt.Printf("2) %+vn", creature) } 

Запустите этот код, чтобы увидеть результат:

Output1) {Species:shark} 2) &{Species:jellyfish} 3) {Species:jellyfish} 

Обратите внимание, что когда мы теперь изменяем значение Species на jellyfish в функции changeCreature, она также изменяет первоначальное значение, которое было определено в функции main. Это связано с тем, что мы передали переменную creature посредством ссылки, дающей доступ к исходному значению и возможность его изменения.

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

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

Нулевые указатели

Все переменные в Go имеют нулевое значение. Это относится и к указателям. Если вы декларировали указатель на тип, но не назначили ему значения, ему будет присвоено нулевое значение nil. nil — это способ сказать, что ничего не было инициализировано.

В следующей программе мы определяем указатель на тип Creature, но не создаем экземпляр Creature и не назначаем его адрес переменной указателя creature. Он будет иметь значение nil, и мы не сможем ссылаться ни на какие поля или методы, определенные для типа Creature:

main.go

package main  import "fmt"  type Creature struct {     Species string }  func main() {     var creature *Creature      fmt.Printf("1) %+vn", creature)     changeCreature(creature)     fmt.Printf("3) %+vn", creature) }  func changeCreature(creature *Creature) {     creature.Species = "jellyfish"     fmt.Printf("2) %+vn", creature) } 

Результат выглядит следующим образом:

Output1) <nil> panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x8 pc=0x109ac86]  goroutine 1 [running]: main.changeCreature(0x0)         /Users/corylanou/projects/learn/src/github.com/gopherguides/learn/_training/digital-ocean/pointers/src/nil.go:18 +0x26     main.main()             /Users/corylanou/projects/learn/src/github.com/gopherguides/learn/_training/digital-ocean/pointers/src/nil.go:13 +0x98         exit status 2 

При запуске программы она выводит значение переменной creature, и это значение <nil>. Затем мы можем вызвать функцию changeCreature, и когда эта функция пытается задать значение поля Species, происходит паника. Это связано с тем, что ни один экземпляр переменной фактически не был создан. Поэтому программе негде хранить значение, и происходит паника.

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

Этот подход обычно применяется для проверки nil:

if someVariable == nil {     // print an error or return from the method or fuction } 

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

main.go

package main  import "fmt"  type Creature struct {     Species string }  func main() {     var creature *Creature      fmt.Printf("1) %+vn", creature)     changeCreature(creature)     fmt.Printf("3) %+vn", creature) }  func changeCreature(creature *Creature) {     if creature == nil {         fmt.Println("creature is nil")         return     }      creature.Species = "jellyfish"     fmt.Printf("2) %+vn", creature) } 

Мы добавили проверку в changeCreature, чтобы посмотреть, имеет ли аргумент creature значение nil. Если это так, программа выводит сообщение creature is nil и выходит из функции. Если это не так, программа изменяет значение поля Species. Если мы запустим программу, результат будет выглядеть следующим образом:

Output1) <nil> creature is nil 3) <nil> 

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

Наконец, если мы создадим экземпляр типа Creature и назначим его переменной creature, программа изменит значение ожидаемым образом:

main.go

package main  import "fmt"  type Creature struct {     Species string }  func main() {     var creature *Creature     creature = &Creature{Species: "shark"}      fmt.Printf("1) %+vn", creature)     changeCreature(creature)     fmt.Printf("3) %+vn", creature) }  func changeCreature(creature *Creature) {     if creature == nil {         fmt.Println("creature is nil")         return     }      creature.Species = "jellyfish"     fmt.Printf("2) %+vn", creature) } 

Теперь у нас имеется экземпляр типа Creature, программа будет выполняться надлежащим образом, и мы получим ожидаемый результат:

Output1) &{Species:shark} 2) &{Species:jellyfish} 3) &{Species:jellyfish} 

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

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

Получатели указателей методов

Приемник в go — это аргумент, определенный в декларации метода. Посмотрите на следующий код:

type Creature struct {     Species string }  func (c Creature) String() string {     return c.Species } 

Приемником в этом методе является c Creature. Это показывает, что экземпляр c имеет тип Creature, и что вы будете ссылаться на этот тип через эту переменную экземпляра.

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

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

Добавим метод Reset в наш тип Creature. Этот метод будет задавать для поля Species пустую строку:

main.go

package main  import "fmt"  type Creature struct {     Species string }  func (c Creature) Reset() {     c.Species = "" }  func main() {     var creature Creature = Creature{Species: "shark"}      fmt.Printf("1) %+vn", creature)     creature.Reset()     fmt.Printf("2) %+vn", creature) } 

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

Output1) {Species:shark} 2) {Species:shark} 

Обратите внимание, что хотя в методе Reset мы задали для Species пустую строку, при выводе значения переменной creature в функции main это поле по-прежнему имеет значение shark. Это связано с тем, что мы определили метод Reset как имеющий приемник значения. Это означает, что у метода будет доступ только к копии переменной creature.

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

main.go

package main  import "fmt"  type Creature struct {     Species string }  func (c *Creature) Reset() {     c.Species = "" }  func main() {     var creature Creature = Creature{Species: "shark"}      fmt.Printf("1) %+vn", creature)     creature.Reset()     fmt.Printf("2) %+vn", creature) } 

Обратите внимание, что мы добавили звездочку (*) перед названием типа Creature при определении метода Reset. Это означает, что экземпляр Creature, который передается в метод Reset, теперь является указателем, и когда мы изменим его, это повлияет на оригинальный экземпляр соответствующей переменной.

Output1) {Species:shark} 2) {Species:} 

Теперь Reset изменил значение поля Species.

Заключение

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