Я уже когда-то добавлял пост про указатели в C – C: указатели — подробный разбор, но было это достаточно давно, да и Си всё-таки не совсем Go, хотя в плане указателей разницы нет.
Тем не менее – рассмотрим указатели в отдельном посте.
Что такое указатель?
Кратко, указатель – это переменная, которая хранит адрес памяти другой переменной, в которой хранится некое значение.
Хотя более правильной формулировкой было бы: указатель – это переменная, которая хранит адрес памяти, который используется другой переменной для хранения данных.
Содержание
Пример указателя
Возьмём самый простой пример с использованием указателя:
package main import "fmt" func main() { a := 1 b := &a fmt.Println("A: ", a) fmt.Println("B: ", b) fmt.Println("B: ", *b) }
Тут:
- создаём переменную с именем
a
, типа integer, со значением 1 - создаём переменную с именем
b
, типа указатель на integer (см. ниже) - и выводим значения:
- сначала просто значение переменной
a
- потом значение(!), или содержимое переменной
b
- получаем значение
a
, на которую ссылаетсяb
(*
и&
расммотрим ниже)
- сначала просто значение переменной
Выполняем:
[simterm]
$ go run pointers_example.go A: 1 B: 0xc0000140e8 B: 1
[/simterm]
Во второй строке мы видим адрес участка памяти, на который указывает указатель b
.
В третьей строке – мы получаем значение из этого участка.
Можно было бы выполнить инициализацию переменных более наглядно, с указанием типов, вместо использования :=
, тогда код выглядел бы так:
... func main() { var a int = 1 var b *int = &a fmt.Println("A: ", a) fmt.Println("B: ", b) fmt.Println("B: ", *b) } ...
В строке var b *int = &a
мы указываем, что переменная b
вляется указателем на integer данные.
Аналогично создаётся указатель на строковые данные – в типе данных переменной (указателя) вместо *int
указываем *string
:
... var c *string fmt.Printf("The C var type: %T, default value: %v\n", c, c) var d string = "This is a string" c = &d fmt.Printf("The C var type: %T, default value: %v, string value: %s\n", c, c, *c) ...
Тут:
- создаём переменную
с
, указываем тип указатель на строковые данные - с помощью
Printf()
спецификаторов выводим тип переменнойс
(%T
), и её значение (%v
) - создаём переменную
d
типа string со значением “This is a string“ - задаём переменной
c
адрес памяти переменнойd
- с помощью
Printf()
спецификаторов выводим тип переменнойс
(%T
), её значение, и значение, которое находится по адресу, который хранится вc
Выполняем:
[simterm]
$ go run pointers_example.go The C var type: *string, default value: <nil> The C var type: *string, default value: 0xc0000101e0, string value: This is a string
[/simterm]
Операторы *
и &
Мы уже видели их использование в примерах выше, кратко остановимся на них.
Оператор *
выполняет разыменование указателя.
Под разыменованием имеется ввиду, что выполняется получение значения не самого указателя (в котором хранится адрес, на который ссылается указатель), а значение из участка памяти, на который указатель… указывает 🙂
Вернёмся к предыдущему примеру:
... fmt.Printf("The C var type: %T, default value: %v, string value: %s\n", c, c, *c) ...
Тут:
default value: %v
ис
– выводит значение, которое хранится в переменнойс
– адрес памяти, на который ссылаетсяc
string value: %s
и*с
– выводит значение, полученное после обращения к адресу изc
Оператор &
возвращает адрес переменной.
Например, добавим к нашему предыдущему примеру ещё одну строку, и отобразим адреса, используя Printf()
и модификатор %p
:
... var c *string fmt.Printf("The C var type: %T, default value: %v\n", c, c) var d string = "This is a string" c = &d fmt.Printf("The C var type: %T, default value: %v, string value: %s\n", c, c, *c) fmt.Printf("The D adress: %p\nThe C address: %p\nThe C value: %v\n", &d, &c, c) ...
Проверяем:
[simterm]
$ go run pointers_example.go The C var type: *string, default value: <nil> The C var type: *string, default value: 0xc0000101e0, string value: This is a string The D adress: 0xc0000101e0 The C address: 0xc00000e028 The C value: 0xc0000101e0
[/simterm]
Тут мы получили адрес переменной d
– 0xc0000101e0, переменной с
– у которой свой адрес – 0xc00000e028, но при этом с
хранит адрес переменной d
– 0xc0000101e0.
Собственно, инициализация данных в указателе с
выполняется именно через получение адреса переменной d
:
... c = &d ...
Функция new()
Кроме объявления и инициализации указателя с помощью var pointername *type
– можно использовать встроенную функцию Go – new()
, которая первым аргументом принимает тип данных, выделяет под этот тип память, и возвращает указатель на этот адрес:
... a := 1 b := new(int) fmt.Printf("A: %d, B: %v, %v\n", a, b, *b) b = &a fmt.Printf("A: %d, B: %v, %v\n", a, b, *b) ...
Тут:
- создаём переменную
a
со значением 1 - создаём переменную
b
, которой изnew()
возвращается указатель на участок памяти под integer тип, и в которой пока содержится 0 (т.к. память проинициализирована, но не может хранить nil данных –new()
заносит туда ноль) - переназначаем
b
адрес переменнойa
Проверяем:
[simterm]
$ go run pointers_example.go A: 1, B: 0xc000014100, 0 A: 1, B: 0xc0000140e8, 1
[/simterm]
Изменение значения указателя
Не совсем верно говорить “изменение значения указателя”, т.к. в указателе хранится адрес.
Но используя указатель – мы можем изменить значение переменной, на которую ссылается указатель.
Например:
... a := 1 b := &a fmt.Println("A: ", a) fmt.Println("B: ", *b) *b = 2 fmt.Println("B: ", *b) ...
Тут:
- присваиваем
a
значение 1 - присваиваем
b
адресa
- выводим значение
a
- выводим значение, на которое указывает
b
- меняем значение в памяти, на которую указывает
b
, на 2 - выводим новое значение
Проверяем:
[simterm]
$ go run pointers_example.go A: 1 B: 1 B: 2
[/simterm]
Передача указателя в функцию
Указатели можно так же использовать как аргумент функции.
Например:
package main import "fmt" func setVal(b *int) { *b = 2 } func main() { a := 1 b := &a fmt.Println("Init values") fmt.Println("A: ", a) fmt.Println("B: ", *b) setVal(b) fmt.Println("Changed values") fmt.Println("A: ", a) fmt.Println("B: ", *b) }
Тут мы создаём функцию setVal()
, которая в виде аргумента принимает указатель на integer, и меняет значение по адресу, который ей передан в указателе.
Проверяем:
[simterm]
$ go run pointers_example.go Init values A: 1 B: 1 Changed values A: 2 B: 2
[/simterm]
После вызова setVal()
– и a
, и b
выводят новое значение.
Более того – мы могли бы передать в setVal()
просто ссылку на a
:
... func setVal(b *int) { *b = 2 } func main() { a := 1 fmt.Println("Init values") fmt.Println("A: ", a) setVal(&a) fmt.Println("Changed values") fmt.Println("A: ", a) }
Результат:
[simterm]
$ go run pointers_example.go Init values A: 1 Changed values A: 2
[/simterm]
Функции: передача по ссылке и передача по значению
Немного оффтоп, но используя пример выше можно продемонстрировать разницу между передачей аргумента по ссылке и передачу аргумента по значению.
Обновим его:
... func setVal(b *int, c int) { *b = 2 c = 4 fmt.Printf("B from setVal(). Poiner to: %p, val: %v\n", b, *b) fmt.Printf("C from setVal(). Addr: %p, val: %v\n", &c, c) } func main() { a := 1 b := &a c := 3 fmt.Println("Init values") fmt.Printf("A from main(). Addr: %p, val: %v\n", &a, a) fmt.Printf("B from main(). Poiner to: %p, val: %v\n", b, *b) fmt.Printf("C from main(). Addr: %p, val: %v\n", &c, c) fmt.Println("Changed values") setVal(b, c) fmt.Printf("A from main(). Addr: %p, val: %v\n", &a, a) fmt.Printf("B from main(). Poiner to: %p, val: %v\n", b, *b) fmt.Printf("C from main(). Addr: %p, val: %v\n", &c, c) }
Тут:
- получаем адрес переменной
a
, и её значение - получаем адрес, который хранится в
b
, и значение из этого участка памяти - получаем адрес переменной
c
, и её значение - вызываем
setVal()
, в которую передаём значениеa
по ссылке в виде указателяb
, аc
– в виде значения - из
setVal()
получаем адрес, который хранится вb
, и значение из этого участка памяти - из
setVal()
получаем адрес переменнойc
и значение из этого участка памяти - из
main()
получаем адрес переменнойa
и значение из этого участка памяти - из
main()
получаем адрес, который хранится вb
, и значение из этого участка памяти - из
main()
получаем адрес переменнойc
и значение из этого участка памяти
Проверяем:
[simterm]
$ go run pointers_example.go Init values A from main(). Addr: 0xc0000140e8, val: 1 B from main(). Poiner to: 0xc0000140e8, val: 1 C from main(). Addr: 0xc000014100, val: 3 Changed values B from setVal(). Poiner to: 0xc0000140e8, val: 2 C from setVal(). Addr: 0xc000014130, val: 4 A from main(). Addr: 0xc0000140e8, val: 2 B from main(). Poiner to: 0xc0000140e8, val: 2 C from main(). Addr: 0xc000014100, val: 3
[/simterm]
Тут:
- изначально у нас
a
расположена по адресу 0xc0000140e8 со значением 1 b
указывает на этот же участок 0xc0000140e8, и возвращает то же значение 1c
расположена по адресу 0xc000014100 со значением 3- вызываем
setVal()
b
вsetVal()
по прежнему указывает на 0xc0000140e8, в котором значение теперь 2c
вsetVal()
получает свой собственный адрес 0xc000014130, в котором хранится значение 4a
вmain()
теперь содержит значение из адреса 0xc0000140e8, которое теперь равно 2b
вmain()
аналогичнаsetVal()
– ссылается на тот же участок, содержит то же значение- в
main()
дляc
ничего не изменилось, так какsetVal()
менял значение c по адресу 0xc000014130, аc
вmain()
находится по адресу 0xc000014100
Указатели так же часто используются со структурами, но о них – в следующий раз.