Я уже когда-то добавлял пост про указатели в 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ис— выводит значение, которое хранится в переменнойс— адрес памяти, на который ссылаетсяcstring 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
Указатели так же часто используются со структурами, но о них — в следующий раз.




