Почему при изменении массива и слайса переданных в функцию у них разное поведение?

Рейтинг: 0Ответов: 2Опубликовано: 16.08.2023

Вычитал на go.dev что структура slice имеет 3 поля: указатель на массив, len, cap. Но как устроен обычный массив я так и не нашел но подозреваю что он состоит из указателя и len.

Также мне известно что при передаче данных в функцию то они просто копируются (без явного указания что этого указатель и при этом это не map)

У меня возник вопрос, что будет если передать массив и слайс в функцию которая их изменит и после посмотреть содержимое изначальных переменных и оказалось что поведут они себя по разному

Пример:

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    slice := []int{1, 2, 3, 4}
    testSlice(slice)
    fmt.Println("slice main:", slice, unsafe.Pointer(&slice))

    array := [4]int{1, 2, 3, 4}
    testArray(array)
    fmt.Println("array main:", array, unsafe.Pointer(&array))
}

func testSlice(slice []int) {
    slice[0] = 0
    fmt.Println("slice func:", slice, unsafe.Pointer(&slice))
}

func testArray(array [4]int) {
    array[0] = 0
    fmt.Println("array func:", array, unsafe.Pointer(&array))
}

Данных код выведет следующий результат:

slice func: [0 2 3 4] 0xc000094078
slice main: [0 2 3 4] 0xc000094060
array func: [0 2 3 4] 0xc0000d0080
array main: [1 2 3 4] 0xc0000d0060

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

Ответы

▲ 1

Slice не хранит никаких данных, он просто описывает раздел базового массива. Изменение элементов slice изменяет соответствующие элементы базового массива. Другие фрагменты, которые используют один и тот же базовый массив, увидят эти изменения.

Вот тут все описано https://go.dev/tour/moretypes/8

▲ 1

Массив в Golang рассматривается как структура с фиксированным числом полей одного и того же типа. Число элементов массива хранится в типе, поэтому массивам, в отличие от срезов, не нужно хранить размер и cap в значении. Значение массива - область памяти размером размер_элемента*число_элементов.

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

Про реализацию. Как именно кодогенератор обрабатывает значения типа [N]T, я не нашел, но то, как устроена память для массива, можно узнать из пакета reflect. Метод Value.Index(int i) возвращает i-ый элемент массива, обёрнутый в объект типа Value:

    case Array:
        tt := (*arrayType)(unsafe.Pointer(v.typ()))
        if uint(i) >= uint(tt.Len) {
            panic("reflect: array index out of range")
        }
        typ := tt.Elem
        offset := uintptr(i) * typ.Size()

        // Either flagIndir is set and v.ptr points at array,
        // or flagIndir is not set and v.ptr is the actual array data.
        // In the former case, we want v.ptr + offset.
        // In the latter case, we must be doing Index(0), so offset = 0,
        // so v.ptr + offset is still the correct address.
        val := add(v.ptr, offset, "same as &v[i], i < tt.len")
        fl := v.flag&(flagIndir|flagAddr) | v.flag.ro() | flag(typ.Kind()) // bits same as overall array
        return Value{typ, val, fl}

Другими словами, указатель на i-ый элемент массива равен указатель_на_массив + i*размер_элемента