Урок курса

Разделяемый базовый массив, 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):] пуст) — та же ситуация.

Задача 1

Дан код:

a := []int{10, 20, 30, 40}
sub := a[1:3]
sub[0] = 99
fmt.Println(a[1])

Что выведет fmt.Println(a[1])?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 2

Что возвращает встроенная функция copy(dst, src)?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 3

Что выведет следующая программа? Введите ровно те строки, которые появятся в stdout, каждую с новой строки.

package main

import "fmt"

func main() {
    src := []int{5, 6, 7}
    dst := make([]int, 2)
    n := copy(dst, src)
    fmt.Println(dst)
    fmt.Println(n)
}
Без регистрации · результат не сохраняется

Задача 4

Удаление без изменения исходных данных

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

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

Что нужно сделать

Допишите removeCopy. Функция получает срез и корректный индекс, создаёт отдельный срез без элемента по этому индексу и не меняет исходные данные. Используйте make и два вызова copy. После вызова программа изменит первый элемент результата на 1000 больше — исходный срез при этом должен остаться прежним.

Формат ввода

Число n (n >= 2), затем n целых чисел и индекс удаления.

Формат вывода

Первая строка — исходный срез. Вторая — результат без выбранного элемента после контрольного изменения его первого элемента.

Пример

Ввод:
5
10 20 30 40 50
2

Вывод:
10 20 30 40 50
1010 20 40 50
РешениеGo

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 5

Наблюдение за подсрезом

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

Представьте, что у вас есть длинная лента данных, а кто-то вырезал из неё узкое окно — два соседних элемента посередине. Это окно живёт не само по себе: оно по-прежнему опирается на ту же ленту. Если теперь в это окно дописать новое значение, оно встанет ровно в ту ячейку исходной ленты, которая следовала сразу за правым краем окна. Лента об этом «не знала» и не давала согласия — но именно в ней теперь лежит новое число.

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

Что уже дано

Входной срез всегда содержит не менее четырёх целых чисел.

Что нужно сделать

Реализуйте ObserveWindow(values []int, added int) [][]int. Возьмите подсрез values[1:3], добавьте в него added через append и верните [][]int{values, window}. В этом диапазоне у подсреза есть ёмкость для добавления, поэтому добавленное значение должно стать видимым и в исходном срезе.

Ввод и вывод

Функция получает срез и одно число. Она возвращает исходный срез после наблюдаемого изменения и получившееся окно. Читать stdin и печатать результат не нужно.

РешениеGo

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задачи к уроку

Все задачи курса →

Продолжить с сохранением прогресса

Перейдите к уроку в учебном кабинете.

Перейти к интерактивному уроку