«В Go есть указатели. Указатель содержит адрес в памяти значения ». НО ПОЧЕМУ ?!

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

Что разработчики ошибаются в отношении указателей

  • Они не конкретны. Разработчики могут сказать: «Используйте указатель», имея в виду похожие, но разные вещи.
  • Они вызывают оба указателя & myVar и * myVar, оставляя новичков в замешательстве.
  • Они ссылаются на базовое значение как на сам указатель.
  • Они ошибочно полагают, что нулевое значение указателя является нулевым значением типа указателя.

К сожалению, это запутывает воду для новичков, потому что * myVar является указателем на значение myVar, а & myVar генерирует указатель на адрес памяти myVar.

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

Так что же такое указатели на самом деле?

Указатель - это просто переменная, содержащая адрес другой переменной.

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

Мы также можем думать об указателе как о следе, который ведет нашу программу к значению. У нас есть указатель, показывающий нам, где проходит тропа (*), и указатель (&). Мне нравится эта аналогия, потому что вы можете думать о маленькой указывающей стрелке каждый раз, когда вы видите * T, или о создании знака стрелки каждый раз, когда вы видите & T.

Через Путешествие по го:

*:

Тип *T - это указатель на значение T. Его нулевое значение nil.

&:

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

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

package main
import (
 "fmt"
)
func noPointer() string {
 return "string"
}
func pointerTest() *string {
 return nil // You cannot return nil from a function that returns a string
 // but you can return nil from a function that returns a pointer to a string!
 // The zero value of a pointer is nil
}
func pointerTestTwo() *string {
 s := "string" // &"string" doesn't work
 return &s
}
func main() {
 fmt.Println(noPointer())      // prints string
 fmt.Println(pointerTest())    // prints <nil>
 fmt.Println(pointerTestTwo()) // prints something like 0x40c158 (an address in memory)
s := pointerTestTwo() // now s holds an address in memory for a variable
 fmt.Println(s)        // something like 0x40c138
 sp := *s              // now sp holds the value found at the address s holds
fmt.Println(sp) // string
}

Запустить этот код

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

Зачем нужны указатели?

Тем, кто работает с такими языками, как JavaScript, может быть сложно понять, зачем вам вообще может понадобиться указатель. Разве мы не всегда хотим работать с базовым значением, а не с адресом, по которому оно хранится в памяти? Что мы будем с этим делать?

В 1960-х указатели давали программистам возможность передать адрес переменной в памяти, а затем разыменовать ее для манипуляций по мере необходимости. Это обеспечило способ создания более сложных структур данных, при этом потребляя меньше памяти. Гарольду Лоусону приписывают изобретение указателей. Он написал основополагающую статью по теме под названием Обработка списков PL / I (дополнительные ресурсы см. В конце) .

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

Это уже не 1960-е, но мы по-прежнему озабочены эффективным использованием памяти и эффективностью. Go был написан как очень простой язык, поэтому логично, что он позволяет программистам передавать информацию таким образом. Тем не менее, GoLang мог бы сделать эту работу за кулисами и сделать так, чтобы это выглядело так, как будто это не так, верно?

В Go все передается по значению. Каждый раз, когда вы что-то передаете функции, функция получает значение, но не имеет доступа к исходному объекту. Посмотрите, что я имею в виду, набрав следующий код out на игровой площадке Go!

package main
import (
 "fmt"
)
type User struct {
 Name string
 Pets []string
}
func (u User) newPet() {
 u.Pets = append(u.Pets, "Lucy")
 fmt.Println(u)
}
func main() {
 u := User{Name: "Anna", Pets: []string{"Bailey"}}
 u.newPet()     // {Anna [Bailey Lucy]} -- the user with a new pet, Lucy!
 fmt.Println(u) // Hey, wait! Where did Lucy go?
}

Беги на игровой площадке го

Здесь я создал функцию, чтобы дать пользователю нового питомца, «Люси», и я создал пользователя «Анна».

Люси была успешно добавлена ​​к домашним животным Анны в функции newPet (), но когда мы напечатали пользователя в следующей строке, у нее не было своего нового питомца!

Я передал значение пользователя newPet () и дал ей нового питомца. Мы по-прежнему не изменили (или изменили) первоначального пользователя. Если мы вернемся к нашему коду, мы могли бы написать его так:

func (u2 User) newPet() {
 u2.Pets = append(u2.Pets, "Lucy") 
 fmt.Println(u2) // {Anna [Bailey Lucy]} -- user with a new pet, Lucy, but this user is not the same as the one in main()! 
}
func main() {
u := User{Name: "Anna", Pets: []string{"Bailey"}}
 u.newPet() 
 fmt.Println(u) // The user still has one pet. 
}

А теперь попробуйте это:

package main
import (
 "fmt"
)
type User struct {
 Name string
 Pet  []string
}
func (p2 *User) newPet() {
 fmt.Println(*p2, "underlying value of p2 before")
 p2.Pet = append(p2.Pet, "Lucy")
 fmt.Println(*p2, "underlying value of p2 after")
}
func main() {
u := User{Name: "Anna", Pet: []string{"Bailey"}} // this time we'll generate a pointer!
 fmt.Println(u, "u before")
p := &u // Let's make a pointer to u!
p.newPet()
 fmt.Println(u, "u after")
// Does Anna have a new pet now? Yes!
}

Запустите этот код на игровой площадке Go

С помощью указателей мы передаем адрес памяти пользователя в newPet () и меняем пользователя, а не его копию.

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

Если кто-то спросит вас, почему в Go используются указатели, вы можете сказать им:

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

Некоторые правила использования

Что это говорит нам о том, как использовать указатели?

  • Если мы хотим, чтобы метод изменил свой приемник, мы должны использовать указатель!
  • Если у нас есть очень большой объем данных, мы можем передать значение ссылки на его местоположение для повышения производительности.
  • Если мы не хотим изменять некоторые данные, значение получателя может быть передано вместо указателя на его получателя - как в нашем первом примере с newPet (). В этом случае получатель рассматривается больше как аргумент.
  • Основная причина использования (или неиспользования) указателей должна определяться доступом и тем, как мы хотим его использовать!

Указатели: параллелизм

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

Выводы

Надеюсь, вам понравилось это обсуждение указателей GoLang. Если у вас есть вопросы или я что-то упустил, дайте мне знать в комментариях! Спасибо!

Дополнительные ресурсы

Указатели в GO

Указатели в информатике