append и мутируемость слайса
package main
import (
"fmt"
"log"
)
func main() {
arr := make([]int, 3, 3)
fmt.Println(arr, len(arr), cap(arr))
log.Printf("%p", arr)
addElems(arr)
fmt.Println(arr, len(arr), cap(arr))
log.Printf("%p", arr)
}
func addElems(arr []int) {
log.Printf("%p", arr)
arr = append(arr, 4)
arr = append(arr, 5)
arr = append(arr, 6)
fmt.Println(arr, len(arr), cap(arr))
log.Printf("%p", arr)
}
Слайс мутирует внутри функции, так устроен гоу, но если если делать append, то всё работает иначе. я думаю, это вероятно, потому что append возвращает новый адрес и вставляет в переменную arr, но не понятно почему сама arr не мутировала.
Я решил сделать другой пример
package main
import (
"fmt"
"log"
)
func main() {
arr := make([]int, 3, 8)
fmt.Println(arr, len(arr), cap(arr))
log.Printf("%p", arr)
addElems(arr)
fmt.Println(arr, len(arr), cap(arr))
log.Printf("%p", arr)
}
func addElems(arr []int) {
log.Printf("%p", arr)
arr = append(arr, 4)
arr = append(arr, 5)
arr = append(arr, 6)
fmt.Println(arr, len(arr), cap(arr))
log.Printf("%p", arr)
}
Тут append ни разу новый адрес не вернул, почему тогда на выходе из функции arr один, а в после работы функции в меине он уже другой?
Ответы (3 шт):
append работает с копией а не с базовым массивом
И если нужно чтобы append новый адрес вернул, добавьте больше 3х значений.
append создает новый слайс c cap= cap(старый слайс)*2 а пока ему cap старого хватает, он в него и аппендит.
Получается, чтобы адрес изменился нужно добавлять больше чем cap(arr)
func main() {
arr := make([]int, 3, 8)
// fmt.Println(arr, len(arr), cap(arr))
// log.Printf("%p", arr)
addElems(arr)
// fmt.Println(newArr, len(newArr), cap(newArr))
// log.Printf("%p", newArr)
}
func addElems(arr []int) {
log.Printf("%p", arr)
arr = append(arr, 4)
arr = append(arr, 4)
arr = append(arr, 4)
log.Printf("%p", arr)
arr = append(arr, 4)
arr = append(arr, 4)
arr = append(arr, 4)
log.Printf("%p", arr)
arr = append(arr, 4)
arr = append(arr, 4)
log.Printf("%p", arr)
arr = append(arr, 4)
arr = append(arr, 4)
arr = append(arr, 4)
log.Printf("%p", arr)
arr = append(arr, 4)
arr = append(arr, 4)
arr = append(arr, 4)
log.Printf("%p", arr)
arr = append(arr, 4)
arr = append(arr, 4)
log.Printf("%p", arr)
fmt.Println(arr, len(arr), cap(arr))
log.Printf("%p", arr)
}
У меня вывод такой:
2024/03/06 13:05:50 0xc000020200
2024/03/06 13:05:50 0xc000020200
2024/03/06 13:05:50 0xc00011a000
2024/03/06 13:05:50 0xc00011a000
2024/03/06 13:05:50 0xc00011a000
2024/03/06 13:05:50 0xc00011c000
2024/03/06 13:05:50 0xc00011c000
[0 0 0 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4] 19 32
2024/03/06 13:05:50 0xc00011c000
Адрес меняется при каждом расширении cap(то есть каждом копировании в новый базовый массив)
Срез - это не указатель. Это структура данных из трёх полей, которая в функцию передаётся по значению.
Вот, смотрите:
package main
import (
"fmt"
)
func main() {
arr := make([]int, 3, 3)
fmt.Println("main: ", arr, len(arr), cap(arr))
fmt.Printf("main: %p->%p\n", &arr, arr)
addElems(arr)
fmt.Println("main: ", arr, len(arr), cap(arr))
fmt.Printf("main: %p->%p\n", &arr, arr)
}
func addElems(arr []int) {
fmt.Printf(" addElems: %p->%p\n", &arr, arr)
arr = append(arr, 4)
arr = append(arr, 5)
arr = append(arr, 6)
fmt.Println(" ", arr, len(arr), cap(arr))
fmt.Printf(" addElems: %p->%p\n", &arr, arr)
}
main: [0 0 0] 3 3
main: 0xc000010018->0xc00001a018
addElems: 0xc000010060->0xc00001a018
[0 0 0 4 5 6] 6 6
addElems: 0xc000010060->0xc000108000
main: [0 0 0] 3 3
main: 0xc000010018->0xc00001a018
В main
переменная arr
находилась по адресу 0xc000010018
, указатель на данные был 0xc00001a018
Но в функции addElems
все операции происходят с совсем другим объектом, лежащим в памяти по адресу 0xc000010060
. То. что в этом объекте изменился указатель, никак не отображается на первом объекте. Аналогично с len
и cap
- они изменились в значении, которое лежит на стеке addElems
, но эти изменения никак не отразились на исходный объект.
В случае make([]int, 3, 8)
указатель не изменился, так как не потребовалась переаллокация. Но и в этом случае исходный слайс ничего не узнал об изменениях в копии, поэтому len
и cap
не изменились, несмотря на добавление трёх элементов.
Если вам нужно, чтобы обновился исходный объект, то нужно либо передавать указатель
func addElems(arr *[]int) {
fmt.Printf(" addElems: %p->%p\n", arr, *arr)
*arr = append(*arr, 4)
*arr = append(*arr, 5)
*arr = append(*arr, 6)
fmt.Println(" ", *arr, len(*arr), cap(*arr))
fmt.Printf(" addElems: %p->%p\n", arr, *arr)
}
либо возвращать изменённый объект
func addElems(arr []int) []int {
fmt.Printf(" addElems: %p->%p\n", &arr, arr)
arr = append(arr, 4)
arr = append(arr, 5)
arr = append(arr, 6)
fmt.Println(" ", arr, len(arr), cap(arr))
fmt.Printf(" addElems: %p->%p\n", &arr, arr)
return arr
}
Стайл гайд рекомендует второй способ, с возвратом обновлённого значения.
сам по себе массив под слайсом arr
мутируется, это можно увидеть, прописав arr = arr[:cap(arr)]
в main
после addElems(arr)
, что создаст новый слайс с длиной, равной вместительности массива, на основе того же массива.
слайс arr
из addElems
, это слайс, созданный на основе слайса из main
.
более подробно о массивах и слайсах можно почитать тут: https://go.dev/blog/slices-intro
можете сократить логи так: fmt.Printf(“%p %v len=%v cap=%v\n”, arr, arr, len(arr), cap(arr))