Коллекции: массивы, срезы и отображения

Разделяемый базовый массив, copy и подсрезы

Содержание курса

В прошлом уроке мы создавали срезы через литералы и make, добавляли элементы через append и итерировались по ним через for range. Один момент остался за скобками: что именно происходит, когда срез передаётся в функцию или присваивается другой переменной? Данные копируются или нет? Ответ на этот вопрос меняет то, как нужно читать почти любой код с срезами.

Срез как указатель, длина и ёмкость; подсрез a[low:high] и общий базовый массив

Когда вы пишете s := []int{10, 20, 30, 40, 50}, Go выделяет массив из пяти int где-то в памяти, а переменная s — это маленькая структура из трёх полей: указатель на первый элемент этого массива, len(s) == 5 и cap(s) == 5. Сам срез — это описание участка, а не контейнер с данными.

Подсрез a[low:high] создаёт ещё одно такое описание того же массива:

package main

import "fmt"

func main() {
    a := []int{10, 20, 30, 40, 50}
    b := a[1:4] // элементы 20, 30, 40

    fmt.Println("b:", b)           // [20 30 40]
    fmt.Println("len(b):", len(b)) // 3
    fmt.Println("cap(b):", cap(b)) // 4
}

b указывает на тот же массив, что и a, просто начало сдвинуто на один элемент вперёд. Длина подсреза — high - low, то есть 4 - 1 = 3. Ёмкость вычисляется по формуле cap(a) - low: здесь cap(a) == 5, поэтому cap(b) == 5 - 1 == 4.

Ключевое следствие: поскольку базовый массив один и тот же, запись через b немедленно видна через a.

package main

import "fmt"

func main() {
    a := []int{10, 20, 30, 40, 50}
    b := a[1:4]

    b[0] = 999 // b[0] — это a[1]

    fmt.Println("a:", a) // [10 999 30 40 50]
    fmt.Println("b:", b) // [999 30 40]
}

b[0] и a[1] — это одна и та же ячейка памяти. Нет никакого копирования; b просто смотрит на участок массива a под другим углом.

Именно здесь чаще всего возникает сюрприз: разработчик создаёт подсрез, изменяет его элементы и не ожидает, что исходный срез тоже изменился. Если такого поведения не нужно — нужна независимая копия, а не подсрез.