Подробное объяснение указателей в Golang
В компьютере есть два важных компонента: CPU и память. CPU в основном отвечает за вычисления, а память отвечает за хранение. В коде, который мы пишем, определенные переменные будут помещены в память во время выполнения. Разные переменные имеют разную длину, а также разные блоки и размеры, занимаемые в памяти.
Вероятно, как показано на рисунке ниже, конечно, реальный вид не такой, здесь просто пример.
Чтобы облегчить CPU выборку данных, любые данные, которые мы храним в памяти, имеют адрес, этот адрес представляет собой расположение переменной в памяти, а переменная, которая хранит этот адрес, является указателем.
Помните: у каждой переменной есть адрес, а значением указателя является адрес.
В языке C больше всего жалоб на указатели вызывает работа указателей и освобождение памяти, что часто приводит к некоторым проблемам с переполнением буфера.
Концепция указателей также сохраняется в Golang, но арифметические операции с указателями выполняться не могут.
В Golang есть три основных концепции указателей:
- адрес указателя
- тип указателя
- значение указателя
Адрес и тип указателя
Переменная-указатель может указывать на адрес памяти любого значения, а объем памяти, занимаемый переменной-указателем, является фиксированным значением, независимо от размера значения, на которое она указывает. На 32-битной машине переменная-указатель занимает 4 байта, а на 64-битной — 8 байт. Когда переменная-указатель определена и не назначена какой-либо переменной, ее значение по умолчанию равно nil
.
В языке Go мы получаем адрес памяти переменной, добавляя символ &
перед переменной, эта операция также называется выборкой адреса. Также *
используйте для представления указателей.
addr := &p
Предположим, что тип переменной p
является T
, где p
представляет собой переменную, адрес которой берется, а результат получает addr
, то есть адрес переменной p
присваивается переменной addr
, тип addr
которой *t
.
package main
import (
"fmt"
)
func main() {
var foo int = 1
fmt.Printf("%p => %T", &foo, &foo)
}
Результат выглядит следующим образом:
0xc00010c008 => *int
Значение указателя
После того, как мы получим указатель переменной через оператор &
, мы можем использовать оператор *
для получения значения указателя.
package main
import (
"fmt"
)
func main() {
foo := 1
addr := &foo
fmt.Printf("addr type: %T\n", addr)
fmt.Printf("the address of pointer: %p\n", addr)
value := *addr
fmt.Printf("value type: %T\n", value)
fmt.Printf("value: %s\n", value)
}
Результат выглядит следующим образом:
addr type: *int
the address of pointer: 0xc0000b2008
value type: int
value: 1
Оператор адреса &
и оператор значения *
являются парой дополнительных операторов, &
принимают адрес и *
принимают значение, на которое указывает адрес, в соответствии с адресом.
Отношения и характеристики переменных, адресов указателей, переменных указателей, адресов и значений следующие:
- Используйте оператор
&
, чтобы взять адрес переменной, вы можете получить переменную-указатель этой переменной. - Значением переменной-указателя является адрес указателя.
- Используйте оператор
*
, чтобы получить значение исходной переменной, на которую указывает переменная-указатель.
Исключение нулевого указателя
package main
import (
"fmt"
)
func main() {
var foo *int
*foo = 1
}
После выполнения приведенного выше кода будет сообщено об ошибке:
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x1054ca2]
Чтобы работать с переменной-указателем, это должен быть указанный адрес. Хотя мы объявили переменную foo
выше, но не присвоили ей адрес памяти, поэтому во время выполнения будет сообщено об исключении нулевого указателя. Обычно мы оптимизируем его до foo := new(int)
.
Перестановка нулевого указателя
package main
import "fmt"
func swap(foo, bar *int) {
bar, foo = foo, bar
}
func main() {
foo, bar := 1, 2
swap(&foo, &bar)
fmt.Println(foo, bar)
}
Как вы думаете, в приведенном выше коде значения переменных foo
и bar
будут заменены? Результат будет:
1 2
Это означает, что между ними нет обмена. При выполнении функции swap
были объявлены 4 переменные, как показано ниже:
Следует отметить, что &foo
и &bar
генерируют новые переменные, когда значение передается в функцию swap
, и имеют новый адрес памяти, но значение, которое они хранят в памяти, является адресом.
После подкачки &foo
указывает на блок памяти 0x004
, соответствующее значение равно 0x002
, &bar
указывает на блок памяти 0x003
, соответствующее значение равно 0x001
, они выполняют обмен, но для адресных блоков 0x001
и 0x002
они не изменились, поэтому окончательный напечатанный результат не изменился.
Примечания по использованию указателей
Использование указателей обеспечивает эффективный доступ к переменным. Если вы не используете указатели, вы можете использовать только методы копирования при передаче значений, поэтому при передаче сложных или больших данных будет потребляться больше производительности.
Но есть несколько моментов, о которых следует помнить при его использовании:
- Старайтесь не использовать указатели для ссылочных типов, таких как
channel
. - Нет необходимости использовать указатели для простых данных, таких как int и bool.
- Лучше не вкладывать указатели, а не один указатель в другой указатель.
- Сценарии, требующие безопасности параллелизма, бесполезные указатели