Указатели в Golang

Указатели в 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 мы переходим на адрес памяти элемента в слайсе и можем его изменить
}

При итерировании по такому слайсу текущий элемент – это не копия значения, а копия указателя. А указатель позволяет обращатсья по изначальному адресу элемента слайса и изменять его.