Указатели в Golang


Для работы с указателями существуют два оператора - & и *
Оператор &
Оператор & позволяет получить адрес ячейки памяти переменной. Объявив переменную var value int = 1
можно получить ее адрес с помощью конструкции &value
var value int = 1
fmt.Println(&value)
// Выведет 0xc000094008
Объявление переменной типа указателя
Но что если нам необходимо записать адрес ячейки другой переменной в какую-то новую переменную? В голанге при присваивании переменной значения и наличии var слева от названия переменной необязательно указывать ее тип, ведь он определится автоматически. Запишем в новую переменную адрес ячейки памяти целого числа и выведем.
var value int = 1
var valueReference = &value
fmt.Println(valueReference)
// Выведет 0xc000094008
В переменной valueReference
находится адрес ячейки переменной value
.
Теперь узнаем, какой тип имеет переменная valueReference
с помощью формата %T
var value int = 1
var valueReference = &value
fmt.Printf("%T \n", valueReference)
// Выведет *int
То есть для записи номера ячейки памяти в какую-то переменную такая переменная должна иметь тип со звездочкой. *int
означает тип указателя на целое число, *string
- тип указателя на строку.
var value int = 1
var valuePointer *int = &value
var str string = "test"
var strPointer *string = &str
fmt.Printf("%T \n", valuePointer) // *int
fmt.Printf("%T \n", strPointer) // *string
Разыменовывание с помощью оператора *
Если нужно вывести значение указателя, то перед переменной такого типа нужно использовать *
var value int = 1
var valuePointer *int = &value
fmt.Println(valuePointer) // 0xc000016080
fmt.Println(*valuePointer) // 1
// 1 - значение типа int по адресу ячейки 0xc000016080
Использование в структурах
Допустим, у нас есть сущность пользователя с полем имейл.
type User struct {
Email string
}
Существует несколько способов создать инстанс сткруктуры:
user1 := User{Email: "user1@gmail.com"}
. Возвращает типmain.User
user1 := new(User)
. Возвращает тип*main.User
user1 := &User{Email: "user1@gmail.com"}
. Возвращает тип*main.User
main.User
и *main.User
это разные типы данных – первый это структура User, а второй – указатель на структуру User. Тем не менее обращение к полям структуры одинаково в обоих случаях.
var user *User = &User{Email: "tester@gmail.com"}
fmt.Println(user.Email) // tester@gmail.com
user.Email = "after@gmail.com"
fmt.Println(user.Email) // after@gmail.com
var user2 User = User{Email: "tester2@gmail.com"}
fmt.Println(user2.Email) // tester2@gmail.com
user2.Email = "after2@gmail.com"
fmt.Println(user2.Email) // after2@gmail.com
Для чего использовать указатели и ссылки?
Для изменения исходных переменных в функциях или другой области видимости.
Объявим функцию, которая принимает юзера и его новый имейл.
func changeEmail(user User, newEmail string) {
user.Email = newEmail
}
Передадим в эту функцию юзера и посмотрим, изменился ли имейл.
var user User = User{Email: "tester@gmail.com"}
fmt.Println(user.Email) // tester@gmail.com
changeEmail(user, "after@gmail.com")
fmt.Println(user.Email) // tester@gmail.com
Как видим, начальная структура не изменилась. Проблема заключается в том, что в func changeEmail(user User, newEmail string)
в качестве user
внутрь функции попадает копия переменной и мы работаем только с ее копией. Можем убедиться в этом, посмотрев адрес ячейки начальной переменной и копии.
func changeEmail(user User, newEmail string) {
fmt.Println(&user) //
user.Email = newEmail
}
var user User = User{Email: "tester@gmail.com"}
fmt.Println(user.Email) // tester@gmail.com
fmt.Println(&user) // 0xc0000701e0
changeEmail(user, "after@gmail.com") // Внутри выведет 0xc000070200. Это другой адрес ячейки, то есть некая копия user.
Адрес user до вызова функции и в самой функции отличаются. В функции фактически создает вторая переменная user
, а после выхода из нее уничтожается, а старая остается прежней. Теперь изменим тип аргумента на указатель типа структуры.
func changeEmail(user *User, newEmail string) {
fmt.Printf("%p \n", user)
user.Email = newEmail
}
var user User = User{Email: "tester@gmail.com"}
fmt.Println(user.Email) // tester@gmail.com
fmt.Printf("%p \n", &user) // 0xc000010200
changeEmail(&user, "after@gmail.com") // Внутри выведет 0xc000010200. То есть внутри функции user это та же ячейка памяти что и переменная на строчке 6
fmt.Println(user.Email) // after@gmail.com
Итерирование по слайсу структур
Объявим и наполним слайс типа User
. При итерировании с помощью range
мы всегда получим копию текущего элемента, а значит, изменение атрибутов структуры не будет иметь силу.
users := []User{
User{
Email: "test@gmail.com",
},
User{
Email: "test2@gmail.com",
},
}
for index, user := range users {
user.Email = "test"
// Фактически не изменяет. user -- это копия элемента users[index]
}
Теперь создадим слайс указателй на тип User
users2 := []*User{
&User{
Email: "test@gmail.com",
},
&User{
Email: "test2@gmail.com",
},
}
fmt.Printf("%v\n", users2) // [0xc000070230 0xc000070240]
for _, user := range users2 {
user.Email = "test" // Фактически изменит
fmt.Printf("%p \n", user) // Копия указателя. По значению хранится адрес ячейки из слайса
// Выведет 0xc000070230, затем 0xc000070240.
// user это копия переменной типа указателя.
// То есть обращаясь по user мы переходим на адрес памяти элемента в слайсе и можем его изменить
}
При итерировании по такому слайсу текущий элемент – это не копия значения, а копия указателя. А указатель позволяет обращатсья по изначальному адресу элемента слайса и изменять его.