JavaRush /Курсы /Go SELF /Углубляемся в работу с cop...

Углубляемся в работу с copy

Go SELF
13 уровень , 1 лекция
Открыта

1. Сигнатура copy: что копирует, сколько и что возвращает

Если честно, у новичка вполне логичный вопрос: «Зачем copy, если есть append? Я же могу append-ом всё собрать». И да — иногда можно. Но как только вы хотите предсказуемо управлять содержимым слайса (особенно внутри одного backing array), append начинает быть похож на друга, который помогает вам переезжать… переставляя коробки случайным образом.

copy хорош тем, что делает одну простую вещь: перезаписывает элементы. Без магии, без изменения длины, без «вдруг выделилась новая память». Это делает copy идеальным инструментом, когда вам нужно «подвинуть мебель в комнате», а не «построить новый дом».

Кстати, copy — одна из базовых встроенных функций Go (вместе с append), появившихся как важное упрощение работы со слайсами.

Начнём с главной формулы дня. В Go есть встроенная функция:

copy(dst, src)

Она копирует элементы из src в dst. Важно запомнить порядок аргументов именно так: куда (dst), потом откуда (src). Если перепутаете — код скомпилируется, но логика будет «в стиле пятничного релиза».

Сколько элементов копируется? Ровно столько, сколько возможно без выхода за границы:

min(len(dst), len(src))

И copy возвращает число реально скопированных элементов. Это не декоративная цифра: она часто нужна, когда вы делаете сдвиги и рассчитываете новую «логическую длину» результата.

Очень важная деталь: copy не меняет len ни у dst, ни у src. Она просто перезаписывает элементы. Если после копирования вам нужно «сделать короче/длиннее» — это отдельный шаг через reslice (например, s = s[:...]) или append.

Мини‑пример на понимание «сколько копирует»:

package main

import "fmt"

func main() {
	dst := make([]int, 2)       // len=2
	src := []int{10, 20, 30}    // len=3

	n := copy(dst, src)
	fmt.Println("n =", n) // n = 2
	fmt.Println(dst)      // [10 20]
}

3. Паттерн №1: настоящая копия слайса

Когда вы пишете:

b := a

вы не копируете элементы — вы копируете заголовок слайса (pointer+len+cap). То есть a и b продолжают смотреть на один и тот же backing array. Меняете b[0] — и a[0] меняется тоже. Иногда это удобно, но часто это источник «паранормальных багов».

Чтобы получить независимую копию данных (с новым backing array), используем связку make + copy:

package main

import "fmt"

func main() {
	tasks := []string{"buy milk", "read book", "write code"}

	snapshot := make([]string, len(tasks)) // новый backing array
	copy(snapshot, tasks)

	snapshot[0] = "buy coffee"

	fmt.Println(tasks)    // [buy milk read book write code]
	fmt.Println(snapshot) // [buy coffee read book write code]
}

Здесь у snapshot другой backing array, поэтому изменения не «протекают» обратно.

Этот паттерн настолько базовый, что его стоит воспринимать как «сделать снимок состояния». В реальном коде вы часто будете клонировать слайс перед тем, как его сортировать, фильтровать или отдавать наружу (например, из функции), чтобы не позволить вызывающему коду случайно испортить ваши данные.

4. Нюанс: len важнее cap для copy

Вот типичная ловушка. Новичок думает так: «Я сделал make([]int, 0, 10), значит у меня много места, сейчас copy туда всё скопирует». Но copy смотрит на len, а не на cap. Если len(dst) == 0, то копировать некуда.

Плохой (но показательный) пример:

package main

import "fmt"

func main() {
	src := []int{1, 2, 3}

	dst := make([]int, 0, 10) // len=0, cap=10
	n := copy(dst, src)

	fmt.Println("n =", n) // n = 0
	fmt.Println(dst)      // []
}

Чтобы копирование состоялось, у dst должна быть длина. Самый прямой вариант — сделать dst нужной длины:

package main

import "fmt"

func main() {
	src := []int{1, 2, 3}

	dst := make([]int, len(src)) // len=3
	copy(dst, src)

	fmt.Println(dst) // [1 2 3]
}

А если вы осознанно хотите использовать уже выделенную ёмкость (cap), можно расширить слайс по длине через reslice, но только если cap позволяет:

package main

import "fmt"

func main() {
	src := []int{1, 2, 3}

	dst := make([]int, 0, 10)
	dst = dst[:len(src)]  // теперь len=3
	copy(dst, src)

	fmt.Println(dst) // [1 2 3]
}

5. copy внутри одного слайса: перекрытия и сдвиги

Теперь самое интересное: copy полезен не только для «сделать клон», но и для «переставить кусок».

Ключевая идея: в Go copy корректно работает даже тогда, когда dst и src перекрываются. Это именно то, что нужно для сдвигов внутри одного backing array: мы берём хвост и перетаскиваем его левее или правее.

Такой подход часто встречается в коде стандартной библиотеки и в реальных примерах потоковой обработки: кусок данных копируют, затем слайс «сдвигают» reslice-ом. Например, в статье про декодер GIF показан характерный приём: берём буфер‑слайс, копируем его в p и затем «отрезаем» уже использованную часть reslice-ом.

Для нас же это означает: copy — это «двигатель», а изменение len — это «коробка передач», и перепутать их местами довольно легко.

Где в этих паттернах живёт copy, а где — изменение длины

Чтобы не путаться, полезно держать в голове простую ментальную модель: copy всегда делает «перезапись», а длина меняется только отдельным шагом.

flowchart TD
    A[Есть слайс s] --> B{Что нужно сделать?}

    B -->|"Сделать независимую копию"| C["make новый слайс нужной len"]
    C --> D["copy(dst, src)"]
    D --> E[Готово: другой backing array]

    B -->|Сдвинуть влево| F["copy(s[i:], s[j:])"]
    F --> G[len пока прежний]
    G --> H[Позже: reslice/логика удаления]

    B -->|"Сдвинуть вправо"| I[Сначала увеличить len: append]
    I --> J["copy(s[i+k:], s[i:])"]
    J --> K[Позже: записать вставку]

Эта схема специально подчёркивает: после сдвига вы часто остаётесь в «промежуточном состоянии», и это нормально. Важна последовательность шагов.

6. Сдвиг влево: подтянуть хвост

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

Сдвиг влево выглядит так:

copy(s[i:], s[i+1:])

То есть мы копируем элементы, начиная с i+1, на позицию i.

Мини‑пример:

package main

import "fmt"

func main() {
	s := []int{1, 2, 3, 4}
	copy(s[1:], s[2:])

	fmt.Println(s) // [1 3 4 4]
}

Обратите внимание на результат: у нас «закрылась дырка» между 2 и 3, но в конце появился дубликат. Это нормально и ожидаемо, потому что len не изменился. Мы просто перезаписали элементы.

Если вы сейчас подумали «а почему Go не укоротил слайс сам?» — потому что copy вообще не имеет права менять длину: она работает как низкоуровневая операция перезаписи. Укорачивать — отдельный шаг, и это будет уже осознанная логика программы.

7. Сдвиг вправо: раздвинуть хвост

Сдвиг вправо нужен, когда вы хотите вставить элемент в середину. Здесь важный момент: нельзя сдвинуть вправо, если у вас нет места по длине. Поэтому первый шаг — увеличить len. Самый простой способ — append (да, мы всё равно дружим с append, просто используем его аккуратно).

Схема вставки «в общих чертах» такая:

  1. увеличить длину на 1
  2. сдвинуть хвост вправо через copy
  3. записать значение в освободившийся слот

Пример, где мы «освобождаем место» на позиции i:

package main

import "fmt"

func main() {
	s := []int{10, 20, 30}

	i := 1
	s = append(s, 0)      // len+1, появилось место
	copy(s[i+1:], s[i:])  // хвост вправо на 1
	s[i] = 99             // вставка

	fmt.Println(s) // [10 99 20 30]
}

Здесь copy перекрывает диапазоны: источник и назначение частично совпадают, и это нормально — copy умеет так работать.

8. Мини‑менеджер задач на []string

Сейчас сделаем небольшой кусочек кода, который ощущается как часть приложения, а не как отдельные абстрактные примеры. Пусть у нас есть минимальный список задач []string, и мы хотим уметь:

  1. делать «снимок» списка (чтобы безопасно показать пользователю, не рискуя, что кто-то поменяет оригинал),
  2. сдвигать блок задач (пока без полного удаления/вставки, только механика сдвига).

Начнём с двух маленьких функций: cloneStrings и shiftLeftByOne.

package main

import "fmt"

// cloneStrings делает независимую копию слайса.
func cloneStrings(src []string) []string {
	dst := make([]string, len(src))
	copy(dst, src)
	return dst
}

// shiftLeftByOne сдвигает элементы на 1 влево, начиная с позиции i.
// Это "полуфабрикат": len не меняем, просто перезаписываем.
func shiftLeftByOne(s []string, i int) {
	if i < 0 || i >= len(s)-1 {
		return
	}
	copy(s[i:], s[i+1:])
}

func main() {
	tasks := []string{"buy milk", "read book", "write code"}

	view := cloneStrings(tasks)
	view[0] = "buy coffee"

	fmt.Println("tasks:", tasks) // tasks: [buy milk read book write code]
	fmt.Println("view: ", view)  // view:  [buy coffee read book write code]

	shiftLeftByOne(tasks, 1)
	fmt.Println("shift:", tasks) // shift: [buy milk write code write code]
}

Здесь есть два важных «прикладных» ощущения.

Во-первых, cloneStrings — это реальная защита от aliasing: вы возвращаете наружу копию, и внешний код не может случайно испортить ваше внутреннее состояние.

Во-вторых, shiftLeftByOne демонстрирует честную механику: сдвиг сделали, но у нас остался «лишний хвост». Пока мы не изменили len, этот хвост виден. В следующих лекциях дня мы научимся доводить такие полуфабрикаты до завершённой операции (удаление/вставка), но фундамент — уже здесь.

9. Типичные ошибки при работе с copy

Ошибка №1: ожидать, что copy изменит len.
Обычно это выглядит так: вы сделали copy(s[i:], s[i+1:]), распечатали s и удивились «почему последний элемент продублировался?». Потому что copy — это только перезапись. Длину меняют append или reslice. Если держать в голове правило «copy не трогает длину», половина путаницы исчезает.

Ошибка №2: перепутать dst и src.
Визуально copy(a, b) выглядит симметрично, но смысл у аргументов разный. Если перепутать, вы можете испортить исходные данные вместо того, чтобы заполнить буфер. Помогает мнемоника: «первый — куда, второй — откуда». Если сомневаетесь, временно переименуйте переменные в dst и src прямо в коде — да, это не самая поэтичная литература, зато работает.

Ошибка №3: пытаться копировать в слайс с len=0, надеясь на cap.
make([]T, 0, 10) даёт место в памяти, но не даёт элементов по длине. Для copy это означает «копировать некуда». Исправление простое: или создайте dst нужной длины сразу, или аккуратно расширьте dst через dst = dst[:n] (и только если cap(dst) >= n).

Ошибка №4: делать сдвиг вправо без увеличения длины.
Если вы попробуете «раздвинуть хвост» через copy(s[i+1:], s[i:]), но len(s) не увеличили, вы либо получите неправильный результат, либо (чаще) вообще ничего полезного. Логика вставки всегда начинается с того, что вы создаёте место по длине — обычно через append.

Ошибка №5: забыть, что под‑слайсы могут делить один backing array.
Иногда вы делаете head := s[:k], потом делаете сдвиги в s, и вдруг head «меняется сам». Он не сам: он смотрит туда же, куда и s. Если вам нужен независимый head, придётся клонировать его через make + copy. И да, это ровно тот случай, когда дополнительная аллокация — не грех, а плата за предсказуемость.

1
Задача
Go SELF, 13 уровень, 1 лекция
Недоступна
Снимок списка
Снимок списка
1
Задача
Go SELF, 13 уровень, 1 лекция
Недоступна
Сколько влезло
Сколько влезло
1
Задача
Go SELF, 13 уровень, 1 лекция
Недоступна
Закрыть дыру
Закрыть дыру
1
Задача
Go SELF, 13 уровень, 1 лекция
Недоступна
Срочная вставка
Срочная вставка
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ