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 шт):

Автор решения: Quester

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(то есть каждом копировании в новый базовый массив)

→ Ссылка
Автор решения: Pak Uula

Срез - это не указатель. Это структура данных из трёх полей, которая в функцию передаётся по значению.

Вот, смотрите:

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
}

Стайл гайд рекомендует второй способ, с возвратом обновлённого значения.

→ Ссылка
Автор решения: Kreket Jot

сам по себе массив под слайсом 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))

→ Ссылка