Golang: указатели — подробный разбор

Автор: | 04/20/2019
 

Я уже когда-то добавлял пост про указатели в 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)
}

Тут:

  1. создаём переменную с именем a, типа integer, со значением 1
  2. создаём переменную с именем b, типа указатель на integer (см. ниже)
  3. и выводим значения:
    1. сначала просто значение переменной a
    2. потом значение(!), или содержимое переменной b
    3. получаем значение a, на которую ссылается b (* и & расммотрим ниже)

Выполняем:

go run pointers_example.go
A:  1
B:  0xc0000140e8
B:  1

Во второй строке мы видим адрес участка памяти, на который указывает указатель 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) 
...

Тут:

  1. создаём переменную с, указываем тип указатель на строковые данные
  2. с помощью Printf() спецификаторов выводим тип переменной с (%T), и её значение (%v)
  3. создаём переменную d типа string со значением «This is a string«
  4. задаём переменной c адрес памяти переменной d
  5. с помощью Printf() спецификаторов выводим тип переменной с (%T), её значение, и значение, которое находится по адресу, который хранится в c

Выполняем:

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

Операторы * и &

Мы уже видели их использование в примерах выше, кратко остановимся на них.

Оператор * выполняет разыменование указателя.

Под разыменованием имеется ввиду, что выполняется получение значения не самого указателя (в котором хранится адрес, на который ссылается указатель), а значение из участка памяти, на который указатель… указывает 🙂

Вернёмся к предыдущему примеру:

...
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)
...

Проверяем:

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

Тут мы получили адрес переменной d0xc0000101e0, переменной с — у которой свой адрес — 0xc00000e028, но при этом с хранит адрес переменной d0xc0000101e0.

Собственно, инициализация данных в указателе с выполняется именно через получение адреса переменной 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) 
...

Тут:

  1. создаём переменную a со значением 1
  2. создаём переменную b, которой из new() возвращается указатель на участок памяти под integer тип, и в которой пока содержится 0 (т.к. память проинициализирована, но не может хранить nil данных — new() заносит туда ноль)
  3. переназначаем b адрес переменной a

Проверяем:

go run pointers_example.go
A: 1, B: 0xc000014100, 0
A: 1, B: 0xc0000140e8, 1

Изменение значения указателя

Не совсем верно говорить «изменение значения указателя», т.к. в указателе хранится адрес.

Но используя указатель — мы можем изменить значение переменной, на которую ссылается указатель.

Например:

...
    a := 1
    b := &a

    fmt.Println("A: ", a)
    fmt.Println("B: ", *b)

    *b = 2
    fmt.Println("B: ", *b)
...

Тут:

  1. присваиваем a значение 1
  2. присваиваем b адрес a
  3. выводим значение a
  4. выводим значение, на которое указывает b
  5. меняем значение в памяти, на которую указывает b, на 2
  6. выводим новое значение

Проверяем:

go run pointers_example.go
A:  1
B:  1
B:  2

Передача указателя в функцию

Указатели можно так же использовать как аргумент функции.

Например:

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, и меняет значение по адресу, который ей передан в указателе.

Проверяем:

go run pointers_example.go
Init values
A:  1
B:  1
Changed values
A:  2
B:  2

После вызова 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)
}

Результат:

go run pointers_example.go
Init values
A:  1
Changed values
A:  2

Функции: передача по ссылке и передача по значению

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

Обновим его:

...
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)
}

Тут:

  1. получаем адрес переменной a, и её значение
  2. получаем адрес, который хранится в b, и значение из этого участка памяти
  3. получаем адрес переменной c, и её значение
  4. вызываем setVal(), в которую передаём значение a по ссылке в виде указателя b, а c — в виде значения
  5. из setVal() получаем адрес, который хранится в b, и значение из этого участка памяти
  6. из setVal() получаем адрес переменной c и значение из этого участка памяти
  7. из main() получаем адрес переменной a и значение из этого участка памяти
  8. из main() получаем адрес, который хранится в b, и значение из этого участка памяти
  9. из main() получаем адрес переменной c и значение из этого участка памяти

Проверяем:

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

Тут:

  1. изначально у нас a расположена по адресу 0xc0000140e8 со значением 1
  2. b указывает на этот же участок 0xc0000140e8, и возвращает то же значение 1
  3. c расположена по адресу 0xc000014100 со значением 3
  4. вызываем setVal()
  5. b в setVal() по прежнему указывает на 0xc0000140e8, в котором значение теперь 2
  6. c в setVal() получает свой собственный адрес 0xc000014130, в котором хранится значение 4
  7. a в main() теперь содержит значение из адреса 0xc0000140e8, которое теперь равно 2
  8. b в main() аналогична setVal() — ссылается на тот же участок, содержит то же значение
  9. в main() для c ничего не изменилось, так как setVal() менял значение c по адресу 0xc000014130, а c в main() находится по адресу 0xc000014100

Указатели так же часто используются со структурами, но о них — в следующий раз.