Увидев эту тему, каждый может подумать, что это обычная менструальная тема. В течение долгого времени я мог в принципе понять различные ситуации, схватывая «перенос ценности», но недавно я столкнулся с более глубокой «ямой» и делюсь ею с вами.
Для начала начнем с самого простого, посмотрим на следующий код:
func main() {
a := []int{7,8,9}
fmt.Printf("len: %d cap:%d data:%+v\n", len(a), cap(a), a)
ap(a)
fmt.Printf("len: %d cap:%d data:%+v\n", len(a), cap(a), a)
}
func ap(a []int) {
a = append(a, 10)
}
можно нажатьздесьЗапустите код, что выводит приведенный выше код?
Я не буду продавать это здесь, и я скажу, что после вызова функции ap для операции добавления a все еще []int{7,8,9}. Причина очень проста: в Go нет передачи по ссылке — это передача по значению, передача по значению означает, что передается копия данных. Это предложение может немного сбить с толку новичков, но реальная ситуация довольно странная, например следующий код:
func main() {
a := []int{7,8,9}
fmt.Printf("len: %d cap:%d data:%+v\n", len(a), cap(a), a)
ap(a)
fmt.Printf("len: %d cap:%d data:%+v\n", len(a), cap(a), a)
}
func ap(a []int) {
a[0] = 1
a = append(a, 10)
}
нажмитездесьЗапустите код, а затем выведите a после ap, вы увидите, что a[0] стало 1, но шапка a по-прежнему 3, кажется, что 10 не добавлено?
Это кажется довольно невероятным, не правда ли, значение передается, почему оно все еще влияет на значение внешней переменной? Само собой разумеется, что оправдано либо изменить, либо не изменить.
Это на самом деле неудивительно, потому что Go и C разные, slice выглядит как массив, но на самом деле является структурой, структура данных в исходном коде такая:
type slice struct {
array unsafe.Pointer
len int
cap int
}
Эта структура на самом деле хорошо понятна.Array — настоящий указатель массива, указывающий на начало непрерывного пространства памяти, а len и cap представляют длину и емкость.
Другими словами, то, что вы, кажется, пишете при передаче параметров в свой код,ap(a []int)
, фактически при компиляции кода этот код становитсяap(a runtime.slice)
Вы можете попытаться понять это так, поставивap(a)
заменитьap(array: 0x123, len: 3, cap: 3)
. Хорошо видно, что три параметра, передаваемые функции ap, являются только тремя значениями и не устанавливают никакой связи с внешней переменной a. Это передача по значению.
Однако вам может быть интересно, почему я изменил значение a[0] и оно будет отражаться снаружи? На самом деле, вы должны быть в состоянии понять это сами, когда увидите это, потому что массив является значением адреса (например, 0x123), этот адрес передается в функцию ap, но адрес 0x123, который он представляет, и 0x123 внешнего a — это адрес памяти, на этот раз когда вы изменяете a[0], вы фактически изменяете значение, хранящееся в адресе 0x123, поэтому, конечно, это будет затронуто снаружи.
В качестве примера предположим, что вы работаете грузчиком на железнодорожной станции и управляете погрузкой и разгрузкой вагонов с 1-го по 3-й (вагоны соединены между собой). Однажды вы заболели и нашли кого-то (называемого А), который временно возьмет на себя управление. Но товары в поезде не трогают желающие, у вас должны быть доказательства. Таким образом, вы делаете копию оригинала сертификата на руку А и одновременно даете А ключ от первой машины. Так как в эти несколько дней он был занят, начальник станции попросил А возглавить и четвертый вагон, так что А также получил оригинал сертификата вагона 4. Через некоторое время, когда вы вернетесь после болезни, у вас по-прежнему будут сертификаты только на автомобили с 1 по 3. Вы можете увидеть, что недавно сделал А в автомобилях с 1 по 3, но вы не имеете права садиться на автомобиль 4.
Приведенный выше пример должен быть хорошей иллюстрацией сценария передачи слайсов.Помните, Go передает только по значению.
Это конец? Однако все не так просто. Недавно столкнулся с этой проблемой на работе. Согласно приведенному выше примеру, хотя вы не имеете права просматривать 4 вагона, если вам интересно, вы можете взглянуть, потому что они непрерывны и взаимосвязаны, так же как массив также является непрерывным фрагментом памяти, поэтому существует такой код:
func main() {
a := []int{}
a = append(a, 7,8,9)
fmt.Printf("len: %d cap:%d data:%+v\n", len(a), cap(a), a)
ap(a)
fmt.Printf("len: %d cap:%d data:%+v\n", len(a), cap(a), a)
p := unsafe.Pointer(&a[2])
q := uintptr(p)+8
t := (*int)(unsafe.Pointer(q))
fmt.Println(*t)
}
func ap(a []int) {
a = append(a, 10)
}
нажмитездесьзапустить код
Хотя внешний колпачок и длина не изменились, функция ap добавляет 10 к тому же адресу памяти, так что я могу использовать более хитрый метод для просмотра? Например, если вы найдете адрес a[2] и переместите длину int назад, функция ap должна добавить 10, верно? Здесь следует отметить, что сервер официального сайта Go 32-битный, поэтому при выполнении этого кода на игровой площадке go int составляет 4 байта.
Исполнительные результаты такие же, как я ожидал!
Но проблемы следуют
func main() {
a := []int{7,8,9}
fmt.Printf("len: %d cap:%d data:%+v\n", len(a), cap(a), a)
ap(a)
fmt.Printf("len: %d cap:%d data:%+v\n", len(a), cap(a), a)
p := unsafe.Pointer(&a[2])
q := uintptr(p)+8
t := (*int)(unsafe.Pointer(q))
fmt.Println(*t)
}
func ap(a []int) {
a = append(a, 10)
}
этот код тыпопробуй снова?
Единственная разница между этим и приведенным выше примером заключается в том, что срез начинается с[]int{7,8,9}
Инициализировать таким образом. Результат выполнения *t равен 3 вместо 10, что сбивает с толку. Зачем? Разве это не непрерывное пространство памяти?
На самом деле проблема, связанная с этим, заключается в проблеме роста слайсов. Когда добавление обнаруживает, что кепки недостаточно, оно перераспределяет пространство. Конкретный исходный код см. в функции Grosslice в runtime/slice.go. Я не буду вдаваться в подробности здесь, только результаты. Когда происходит рост среза, больший объем памяти будет перераспределен на срез, а затем будут скопированы исходные данные, а указатель массива среза будет указывать на новую память. То есть, если предыдущие данные хранятся по адресу памяти 0x0 0x8 0x10, когда не происходит прироста, значение нового добавления будет сохранено в 0x18, но когда происходит прирост, все предыдущие данные копируются в новый адрес 0x1000 0x1008 0x1010, значение нового добавления помещается в 0x1018.
На этот раз вы можете понять, почему иногда вы можете получить данные с небезопасными данными, а иногда нет. Может быть, вы понимаете, почему этот пакет называется небезопасным. Но небезопасно не совсем небезопасно, это означает, что очень легко стать небезопасным, если вы используете неправильную позу. Но если поза элегантна, она на самом деле очень безопасна. Для операций среза, если вы хотите использовать unsafe, не забудьте обратить внимание на то, отправляет ли шапка изменения, что означает миграцию памяти