Как работают указатели в Go

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

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

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

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

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

var myPointer *int32 = &someint

Эта строка создает myPointer, указатель на переменную int32, и инициализирует его с адресом someint. На самом деле указатель не содержит int32, а только адрес.

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

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

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

creature = shark
pointer = 0xc0000721e0

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

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

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

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)
}

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

creature = shark
pointer = 0xc000010200
*pointer = shark

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

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

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)

}

Запустите этот код:

creature = shark
pointer = 0xc000094040
*pointer = shark
*pointer = jellyfish

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

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

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)
}

Вот так выглядит вывод:

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

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

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

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

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

Читайте также: Определение и вызов функций в Go

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

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

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

Вывод выглядит так:

1) {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 – это структура (struct), которая в Species имеет значение shark. *creature – это указатель, а не структура, поэтому его значение – это место в памяти, и это мы передаем в changeCreature().

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

Запустите программу и вы увидите:

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

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

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

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

Читайте также: Обработка паники в Go

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

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

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

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

Вывод выглядит так:

1) <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:

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

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

package main
import "fmt"
type Creature struct {
Species string
}
func main() {
var creature *Creature
fmt.Printf("1) %+v\n", creature)
changeCreature(creature)
fmt.Printf("3) %+v\n", creature)
}
func changeCreature(creature *Creature) {
if creature == nil {

fmt.Println("creature is nil")


return


}

creature.Species = "jellyfish"
fmt.Printf("2) %+v\n", creature)
}

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

1) <nil>
creature is nil
3) <nil>

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

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

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

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

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

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

Получатель указателя

Получатель, или receiver в go – это аргумент, определенный в объявлении метода. Посмотрите на следующий код:

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

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

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

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

Давайте добавим к нашему типу Creature метод Reset, который установит в поле Species пустую строку:

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) %+v\n", creature)
creature.Reset()
fmt.Printf("2) %+v\n", creature)
}

Запустив программу, вы получите:

1) {Species:shark}
2) {Species:shark}

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

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

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) %+v\n", creature)
creature.Reset()
fmt.Printf("2) %+v\n", creature)
}

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

1) {Species:shark}
2) {Species:}

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

Tags: ,

Добавить комментарий