Урок курса
Разделяемый базовый массив, copy и подсрезы
Golang с нуляВ прошлом уроке мы создавали срезы через литералы и 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 под другим углом.
Именно здесь чаще всего возникает сюрприз: разработчик создаёт подсрез, изменяет его элементы и не ожидает, что исходный срез тоже изменился. Если такого поведения не нужно — нужна независимая копия, а не подсрез.
make и copy: создание независимой копии среза
Чтобы получить срез с собственным базовым массивом, нужно сначала выделить память — через make([]T, n) — и затем перенести элементы через copy. Ни одно из этих действий само по себе не даёт полного результата: make([]T, n) создаёт срез длины n, заполненный нулевыми значениями типа T — это не nil-срез, а реальный массив из нулей; данные src в нём появятся только после copy. А copy без предварительно выделенного dst просто некуда писать.
package main
import "fmt"
func main() {
src := []int{10, 20, 30, 40, 50}
dst := make([]int, len(src)) // срез длины 5, элементы пока все равны 0
n := copy(dst, src) // копирует min(len(dst), len(src)) элементов
fmt.Println("скопировано:", n) // 5
fmt.Println("dst:", dst) // [10 20 30 40 50]
dst[0] = 999
fmt.Println("src после изменения dst:", src) // [10 20 30 40 50] — не изменился
}
copy возвращает количество скопированных элементов — это всегда min(len(dst), len(src)). Если dst короче src, скопируется только то, что влезает; остаток src отбрасывается. Если dst длиннее — лишние позиции останутся нулями. Возвращаемое число редко нужно сохранять в переменную, но важно понимать: именно оно говорит, сколько элементов реально перешло.
После copy у dst и src разные базовые массивы. Запись dst[0] = 999 не затрагивает src — что и подтверждает последний fmt.Println.
Одно важное уточнение: сам copy не выделяет память. Если написать var dst []int; copy(dst, src), copy честно скопирует ноль элементов, потому что len(dst) == 0. Память нужно выделить явно через make до вызова copy.
Граница применимости. Для []int — как в примере выше — отдельный базовый массив даёт полную независимость: изменение любого элемента через dst не видно через src и наоборот. Это работает, потому что int копируется как значение. copy всегда копирует элементы именно как значения. Если элементы сами содержат указатели, срезы или map, то скопируется ссылка, а не объект, на который она указывает — оба среза будут разделять вложенные данные. Глубокое копирование таких структур в рамках этого урока не рассматривается.
Функция removeCopy: удаление элемента без изменения исходного среза
Допустим, есть срез []int{10, 20, 30, 40, 50} и нужно получить новый срез без элемента по индексу i, не трогая оригинал. Подсрез здесь не поможет — он разделяет базовый массив. Нужен отдельный массив длиной len(s) - 1, в который копируются два куска: до удаляемого элемента и после него.
package main
import "fmt"
func removeCopy(s []int, i int) []int {
dst := make([]int, len(s)-1)
// копируем элементы до индекса i
copy(dst, s[:i])
// копируем элементы после индекса i, начиная с позиции i в dst
copy(dst[i:], s[i+1:])
return dst
}
func main() {
original := []int{10, 20, 30, 40, 50}
result := removeCopy(original, 2) // удаляем 30
fmt.Println("original:", original) // [10 20 30 40 50]
fmt.Println("result:", result) // [10 20 40 50]
}
Первый copy(dst, s[:i]) записывает элементы s[0], s[1] в dst[0], dst[1]. Второй copy(dst[i:], s[i+1:]) стартует с dst[2] и пишет туда s[3], s[4]. В итоге dst содержит [10, 20, 40, 50].
Почему original не изменился? Потому что dst — это отдельный массив, созданный через make. Когда removeCopy получает срез s, она получает копию описания (указатель + len + cap), но читает элементы исходного массива, не пишет в него. Всё, что функция записывает, уходит в dst.
Один момент, который легко упустить: copy(dst[i:], s[i+1:]) — это тоже подсрез dst, и он указывает на тот же базовый массив dst. Это нормально: мы сдвигаем точку старта записи, чтобы не перезаписать то, что уже положил первый copy. После возврата функции оба подсреза выходят из области видимости, наружу уходит только dst.
Диапазон корректных значений i — от 0 до len(s)-1 включительно. При i == 0 первый copy копирует ноль элементов (s[:0] пуст), и это нормально — copy просто ничего не делает. При i == len(s)-1 второй copy копирует ноль элементов (s[len(s):] пуст) — та же ситуация.
Попробуйте решить
Дан код:
a := []int{10, 20, 30, 40}
sub := a[1:3]
sub[0] = 99
fmt.Println(a[1])
Что выведет fmt.Println(a[1])?
Продолжить с проверкой и прогрессом
Откройте интерактивный раннер с заданиями урока.
