---
title: "Срезы: append, copy и подводные камни"
description: "append в go: рост среза и подводные камни общей памяти, copy и идиома s = append(s, ...), удаление элемента. Примеры запускаются прямо в песочнице."
course: "Go с нуля: базовый синтаксис и конструкции"
url: "https://golangmaster.ru/courses/go-basics/lessons/18-srezy-append/"
published: "2026-09-19"
updated: "2026-09-28"
minutes: 22
---

# Срезы: append, copy и подводные камни

*Курс «Go с нуля: базовый синтаксис и конструкции», урок 18 из 25. ~22 минут. Обновлено 28.09.2026. Источник: https://golangmaster.ru/courses/go-basics/lessons/18-srezy-append/*

Функция append — главный инструмент роста среза, и запросы append go и copy срез go стабильно всплывают у всех, кто добрался до коллекций, — не зря: половина найденных ответов пугает «шарингом памяти». Здесь разберём оба глагола по шагам: append к nil-срезу, рост среза и его цену, подводные камни общей памяти, идиому s = append(s, ...) для функций, copy с правилом минимальной длины, удаление элементов и матрицы. Всё на примерах, которые запускаются прямо в уроке.

### append в go: дописать и присвоить обратно

В [уроке 17](/courses/go-basics/lessons/17-srezy-basics/) мы оставили срез на пороге роста: запись за границей срез не растягивает, len и cap у свежего среза равны, а фраза «запас под будущий append» так и осталась обещанием. Время выполнить его. append — встроенная функция, которая дописывает элементы в конец среза и возвращает обновлённый срез. Синтаксис один для любых типов: s = append(s, 5), names = append(names, "ada"), nums = append(nums, 18, 21, 19) — три числа одним вызовом. Есть и вторая форма, append(s, other...), которая дописывает сразу все элементы другого среза — она пригодится при склейке и удалении.

Две детали, без которых append не понять. Первая: append не меняет срез на месте — он возвращает новый заголовок, и его обязательно присваивать обратно той же переменной: s = append(s, v). Голый вызов append(s, v) без присваивания даже скомпилируется, но результат улетит в никуда: сколько элементов было, столько и осталось. Вторая: дописывать можно в любой срез, включая nil-срез из урока 17 и пустой литерал — язык сам выделит память. Массив из [урока 16](/courses/go-basics/lessons/16-massivy/) прирастать не умеет вовсе: [3]int навсегда [3]int. Вся динамика go-коллекций — срезы, и почти вся динамика — это append.

### Песочница: append к nil-срезу

Запустите: nil-срез из урока 17 оказался не тупиком, а стартовой точкой — append дописал первый элемент, и срез ожил. make не понадобился вовсе, память выделилась сама. Три опыта. Первый: допишите после второй печати s = append(s, 8, 9, 10) — append берёт сколько угодно элементов за раз. Второй: замените старт var s []int на s := []int{} и запустите — вывод не изменится: для append нет разницы между nil-срезом и пустым литералом. Третий: поставьте печать fmt.Println(s) ДО первого append — основной движок напечатает [], резервный — слово <nil>; поэтому в ранимых примерах nil-срез печатается только после того, как в нём что-то появилось.

```go
package main

import "fmt"

func main() {
        var s []int
        fmt.Println("пусто:", len(s) == 0, s == nil)

        s = append(s, 5)
        fmt.Println(s, len(s))

        s = append(s, 6, 7)
        fmt.Println(s, len(s))

        squares := []int{}
        for i := 1; i <= 5; i++ {
                squares = append(squares, i*i)
        }
        fmt.Println(squares, len(squares))
}
```

### Рост среза: len ползёт, cap скачет

Что происходит с памятью, когда append дописывает за пределы cap? Срез устраивает переезд: выделяется новый массив побольше, старые элементы копируются туда, дописанный встаёт следом, а заголовок s начинает смотреть на новый дом. len при этом растёт на единицу, а cap — скачками: язык не добавляет по одной ячейке, а резервирует с запасом, чтобы следующие append обошлись без переезда. В настоящем Go запас растёт примерно удвоением: у пустого среза после первого append cap 1, потом 2, 4, 8 — на сотнях элементов шаг становится щедрее, но идея та же.

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

А вот cap после каждого append в уроке мы печатать не будем — на этом месте движки песочницы расходятся: резервный интерпретатор всегда рисует cap равным len и никакого удвоения не знает. Поэтому следующий блок без кнопки: рост ёмкости один раз стоит увидеть в настоящем Go.

### Рост на экране: len и cap по шагам

```go
package main

import "fmt"

func main() {
        s := []int{}
        for i := 0; i < 8; i++ {
                s = append(s, i)
                fmt.Println("len:", len(s), "cap:", cap(s))
        }
}
```

Кнопки нет: вывод движков расходится. Основной движок песочницы — настоящий Go — печатает len: 1 2 3 4 5 6 7 8 и рядом cap: 1 2 4 4 8 8 8 8: ёмкость прыгает удвоениями, между прыжками стоит на месте. Резервный TS-интерпретатор всегда рисует cap равным len: 1 2 3 4 5 6 7 8 — запаса у него нет. Запустите этот код в локальном Go или в основном движке, чтобы увидеть удвоение; len при этом совпадает в обоих движках, и ранимая версия с одной только len в уроке есть — она в runner-е выше.

### Проверьте себя: рост среза

**Вопрос:** Вы дописали в пустой срез восемь элементов по одному через append. Что вернёт cap(s) в конце в настоящем Go?

- 1: ёмкость не растёт вместе со срезом
- ✅ 8: последний прыжок удвоения был с 4 до 8
- 16: после восьми элементов добавляется ещё запас
- 0: у растущего среза ёмкости нет

Ёмкость растёт скачками примерно удвоением: 1, 2, 4, 8 — после восьми append cap равен 8, и следующие восемь дописываний обойдутся без переезда. len при этом тоже 8. В резервном движке песочницы cap всегда равен len, поэтому вопрос — про настоящий Go и основной движок: там удвоение видно на экране.

### Подводные камни срезов: append пишет в чужую память

Вот главный подводный камень срезов, ради которого урок и назван «подводные камни». Срез — окно: указатель, len, cap. Если два среза смотрят на один массив и у окна есть запас ёмкости (len меньше cap), append НЕ устраивает переезд — он дописывает элемент прямо в общий массив, в ячейку за пределами len окна. А та ячейка может принадлежать другому срезу — и тот увидит в себе чужую правку. Такой шаринг памяти — не баг языка, а прямое следствие устройства среза из урока 17.

Сценарий классической ошибки: вырезали голову s[:2], дописали в неё элемент — и в оригинале s тихо испортился третий элемент. Никакой ошибки в консоли: программа работает, числа слегка неправильные. Такие баги живут в коде неделями, потому что падать нечему.

Разберём, когда append безопасен, а когда опасен. Безопасно, если у среза нет запаса — len == cap: append выделит новый массив и чужую память не заденет; именно поэтому присваивание t := s от свежего литерала в уроке 17 вело себя прилично. Опасно, если окно вырезано операцией среза s[i:j] из более длинного среза — за его len обычно лежат живые элементы оригинала. И тонкость песочницы: резервный движок копирует данные при срезании, поэтому в нём ошибку воспроизвести нельзя — она просто не проявится. Проверяйте пример ниже в основном движке или локальном Go.

### Классическая ошибка: append через окно

```go
package main

import "fmt"

func main() {
        s := []int{1, 2, 3, 4, 5}
        head := s[:2]
        head = append(head, 99)
        fmt.Println(s)
        fmt.Println(head)
}
```

Кнопки нет, и вот почему. Основной движок — настоящий Go: у head len 2 и cap 5, запас есть, и append пишет 99 прямо в общий массив. Срезы печатают [1 2 99 4 5] и [1 2 99]: третий элемент s затёрт, хотя мы «только дописали в голову». Резервный движок копирует данные при срезании, поэтому он печатает s целым — [1 2 3 4 5] — и head как [1 2 99]. Выводы разъехались, урок один: append от вырезки с запасом cap в настоящем Go опасен, а резервный режим эту ошибку прячет. Лекарство — полный срез s[low:high:max] ниже по уроку или явный copy.

### Что выведет код: append и сосед

```go
package main

import "fmt"

func main() {
        s := []int{1, 2, 3}
        t := s
        t = append(t, 9)
        fmt.Println(s)
        fmt.Println(t)
}
```

**Что выведет программа?**

- [1 2 9] и [1 2 9]: append дописал в общий массив
- ✅ [1 2 3] и [1 2 3 9]: append выделил новый массив, сосед цел
- [1 2 3] и [1 2 3]: правка через t вообще не видна
- Ошибку: дописывать в присвоенный срез нельзя

У литерала len 3 и cap 3 — запаса нет, поэтому append устраивает переезд в новый массив: t смотрит на новый дом, s остаётся при своих трёх элементах. Такой вывод дают оба движка, блок ранимый. Проверку «а что, если запас есть» через чистое присваивание устроить не выйдет: вместе с len присваивается и cap, а у свежего литерала они равны. Запас появляется только у вырезок и у make с тремя аргументами — обе формы в этом уроке неранимые и обе разобраны отдельно.

### Идиома s = append(s, ...): срез в функции

Срез — заголовок из трёх слов, и в функцию приезжает копия заголовка, а не копия данных. Из этого следуют два разных поведения. Правка элементов внутри функции видна снаружи: nums[0] = 99 меняет общий массив — мы прогоняли это ранимым примером в уроке 17. А вот append внутри функции снаружи не заметен: функция дописала элемент и переприсвоила свой ЛОКАЛЬНЫЙ заголовок — на этом всё кончилось. Снаружи len остался старым; новый элемент живёт где-то в памяти, но через чужой заголовок не виден.

Отсюда железное правило идиомы: функция, которая растит срез, обязана вернуть его — func grow(s []int) []int — а место вызова обязано присвоить результат обратно: s = grow(s). Так устроен сам append — принимает срез, возвращает срез — и так пишется весь прикладной Go. Правило страхует дважды: вы не потеряете элементы, дописанные в функции, и не удивитесь, почему их «нет».

Пара нюансов напоследок. Передача среза в функцию дешёвая всегда: три машинных слова, данные не копируются, хоть за окном миллион элементов. А функция, которая только читает, тоже берёт []int: nil-срез и пустой срез читаются безопасно — range сделает ноль оборотов, len вернёт 0, это мы проверяли в уроке 17.

### Песочница: append внутри функции

Запустите: badAdd дописал 99 в свою локальную копию заголовка — снаружи len прежний и элемента не видно. grow возвращает срез, и присваивание s = grow(s) делает дописывание видимым: два вызова — и в срезе [1 2 3 42 42]. Опыты. Первый: уберите присваивание (оставьте голый вызов grow(s)) — элемент снова исчезнет: возврат без присваивания ничего не меняет. Второй: добавьте в badAdd печать fmt.Println("внутри:", s, len(s)) после append — внутри длина 4, снаружи 3: заголовки уже разные, данные одни. Третий: замените тело grow на правку s[0] = 42 с возвратом s и уберите присваивания — правка элемента видна снаружи и без присваивания: массив общий, заголовок тут ни при чём.

```go
package main

import "fmt"

func badAdd(s []int) {
        s = append(s, 99)
}

func grow(s []int) []int {
        return append(s, 42)
}

func main() {
        s := []int{1, 2, 3}
        badAdd(s)
        fmt.Println("после badAdd:", s)

        s = grow(s)
        s = grow(s)
        fmt.Println(s, len(s))
}
```

### Копирование среза: функция copy

Вторая половина пары — copy: встроенная функция копирует элементы из одного среза в другой и возвращает число скопированных элементов. Вызов copy(dst, src) копирует ИЗ src В dst: приёмник первый, источник второй — легко перепутать, держите в голове «скопировать В». Правило минимальной длины: копируется min(len(dst), len(src)) элементов, лишнее молча отбрасывается. Если источник короче приёмника, dst не обрезается: скопированная часть ложится в начало, хвост остаётся как был — у свежего make это нули.

Типичные задачи пары. Независимая копия: c := make([]int, len(s)); copy(c, s) — теперь правки c не задевают s, в отличие от t := s, которое делит данные. Склейка двух срезов: t := make([]int, 0, len(a)+len(b)); t = append(t, a...); t = append(t, b...) — форма с запасом, про которую честный callout ниже.

Ручной цикл range делает то же самое: for i, v := range src { dst[i] = v } — но только пока dst не короче src, иначе запись за границей. copy короче и безопаснее на разных длинах: сам отрезает по минимуму. В цикле ещё и помни, что v — копия, и писать надо строго по индексу; copy об этом думает за вас. На маленьких срезах разницы в скорости нет — выбирайте читаемость.

### copy на практике: минимум длин

```go
package main

import "fmt"

func main() {
        src := []int{7, 8, 9, 10, 11}
        dst := make([]int, 3)
        n := copy(dst, src)
        fmt.Println(dst, n)

        tail := make([]int, 4)
        m := copy(tail, []int{1, 2})
        fmt.Println(tail, m)
}
```

Блок ранимый — оба движка дают одинаковый вывод: [7 8 9] 3 и [1 2 0 0] 2. В первом вызове источник длиннее: скопированы первые три элемента, десятка и одиннадцать отброшены, copy вернул 3. Во втором приёмник длиннее: легли два элемента, хвост остался нулями, вернулось 2. Печать среза и числа рядом — полезная привычка: по ней сразу видно, кто кого ограничил, приёмник или источник.

> **Предзаготовка: make([]T, 0, n).** Когда число элементов известно заранее, срез готовят с запасом: make([]string, 0, 100) — пустой срез длины 0 с ёмкостью 100; append пишет в готовые ячейки и не устраивает переездов. В настоящем Go такой вызов даёт len 0 и cap 100. Резервный движок третий аргумент make игнорирует — квирк из урока 17, — и показывает cap 0, поэтому форма остаётся неранимой, без кнопки. Польза от неё в основном движке и локальном Go ощутимая: для известных объёмов это идиома по умолчанию, тот самый «запас под будущий append» из урока 17, только руками.

### Полный срез s[low:high:max]: поводок для append

У операции среза есть полная форма с третьим индексом: guard := s[low:high:max]. Первые два — знакомый полуинтервал, третий задаёт ёмкость результата: cap = max - low. Смысл — поводок. Обычная вырезка head := s[:2] от литерала из пяти элементов получает cap 5 и может дописывать в чужие ячейки — та самая классическая ошибка выше. Вырезка с максимумом head := s[:2:2] получает cap 2 — ровно по len, запаса нет, и первый же append уведёт её в новый массив, не тронув s. Это встроенный предохранитель от шаринга памяти: ставьте max = high, и append от вырезки безопасен по построению.

В настоящем Go форма работает как описано: full := s[1:3:5] даёт len 2 и cap 4 (5-1), а вот head := s[:2:2] при append-е переезжает целиком. Проверить это в песочнице целиком не выйдет: резервный интерпретатор третий индекс молча игнорирует — квирк, известный с урока 17, — и рисует cap по своим правилам, так что формула cap = max - low у него не выдерживает проверки. Поэтому раздел концептуальный, без кнопки: полный срез полезен в основном движке и локальном Go, а в ранимых примерах урока не появляется.

### Проверьте себя: полный срез

**Вопрос:** s := []string{"a", "b", "c", "d", "e"}, затем head := s[:2:2]. Что сделает append(head, "x") в настоящем Go?

- Запишет "x" в общий массив: s станет [a b x d e]
- ✅ Выделит новый массив: s останется [a b c d e], head станет [a b x]
- Вернёт head без изменений: append запрещён при len == cap
- Уронит программу: ёмкость закончилась

Третий индекс ограничил ёмкость: cap(head) = 2 - 0 = 2 при len 2 — запаса нет, и append выделяет новый массив. Оригинал в безопасности — в этом смысл полного среза как предохранителя. Для сравнения: обычная вырезка s[:2] имела бы cap 5, и append вписал бы "x" в общий массив (в основном движке). Резервный движок третий индекс игнорирует, поэтому вопрос — про настоящий Go.

### Удаление элемента: append-форма и копия с конца

Отдельной операции «удалить» в Go нет — её собирают из имеющихся кирпичей. Каноническая форма с сохранением порядка: s = append(s[:i], s[i+1:]...). Читается: возьми всё до i, допиши к нему всё после i, результат присвой обратно. Порядок элементов сохраняется, len уменьшается на один, а в последнем слоте массива остаётся дубль — на печать он не влияет, но помнить про него стоит. Так удаляются и первый (i = 0), и последний (i = len(s)-1) элементы: частных случаев и отдельных форм нет.

Вариант без append — копирование с конца: copy(s[i:], s[i+1:]); s = s[:len(s)-1]. Элементы после i сдвигаются на один влево прямо на месте, срез укорачивается операцией среза. Он чуть экономнее на больших срезах и не собирает промежуточный заголовок, но читается хуже; выбирайте append-форму, пока скорость не доказана профилем.

Честная нота про песочницу: append-форма удаления печатает одинаковый результат в обоих движках — поэтому ранимый пример ниже есть. А copy-вариант на месте в резервном движке НЕ работает: при срезании он копирует данные, сдвига не происходит, и срез остаётся прежним. Проверяйте его в основном движке или локальном Go; в ранимых примерах урока — только append-форма.

### Песочница: удалить элемент

Запустите: из языков удалился rust — порядок сохранился, go и python стоят на своих местах, len стал 2. Дальше два частных случая той же формы над числами: i = 0 срезает голову (10 исчез), затем i = len-1 срезает хвост (40 исчез). Опыты. Первый: удалите из langs ещё и python — индекс после первого удаления сместился, нужно i = len(langs)-1. Второй: опустошите nums циклом for len(nums) > 0 { nums = append(nums[:0], nums[1:]...) } и напечатайте len — срез честно опустеет до нуля. Третий: попробуйте i = 10 на исходных четырёх числах — вырезка за границей: настоящий Go паникует slice bounds, резервный молча зажмёт; правило 0 <= i <= len(s) из урока 17 спасает.

```go
package main

import "fmt"

func main() {
        langs := []string{"go", "rust", "python"}
        i := 1
        langs = append(langs[:i], langs[i+1:]...)
        fmt.Println(langs, len(langs))

        nums := []int{10, 20, 30, 40}
        nums = append(nums[:0], nums[1:]...)
        fmt.Println(nums)

        nums = append(nums[:len(nums)-1], nums[len(nums):]...)
        fmt.Println(nums)
}
```

### Вставьте пропуски: дописать и скопировать

Допишите два вызова: срез растёт через append с присваиванием обратно, данные копируются через copy. Собранная программа печатает «[1 2 3] 2».

```go
s := []int{1, 2}
s = ___(s, 3)
buf := make([]int, 2)
n := ___(buf, s)
fmt.Println(s, n)
```

Подсказка: Первый пропуск — функция роста среза; её результат обязательно присваивается обратно. Второй — функция копирования: приёмник первый, источник второй.

Правильные ответы в пропусках: `append`, `copy`

### Соберите программу: удаление с сохранением порядка

Соберите программу: срез строк, индекс и append-форма удаления. Собранный вывод — «[альфа гамма]».

Правильный порядок строк:

1. `package main`
2. `import "fmt"`
3. `func main() {`
4. `words := []string{"альфа", "бета", "гамма"}`
5. `i := 1`
6. `words = append(words[:i], words[i+1:]...)`
7. `fmt.Println(words)`
8. `}`

### Срезы срезов: матрица, которую можно растить

Двумерные срезы из урока 17 — «рваные» полки разной длины — собирались литералом. Когда размер известен в числах, матрицу собирает make: matrix := make([][]int, rows) даёт rows строк, каждая — пока nil-срез; циклом for i := range matrix строкам выдаётся ширина: matrix[i] = make([]int, cols). Получается плотная сетка rows × cols, готовая под запись по индексам. В nil-строки можно и append-ить — матрица останется рваной, если так задумано.

Приём масштабируется на всё, что «таблица, но растущая»: журнал координат, заказы по дням, сетка температур по неделям. Строки-срезы независимы: matrix[0][1] = 5 заденет только первую строку, а append к matrix добавляет целую строку — она сама срез, со своими len и cap. Перебор — range снаружи, range внутри, как проговаривали в уроке 15 и 17: добавился только make-скелет.

И это последнее место, где срезам нужна помощь. Следующая коллекция курса — карты map из урока 19: там элементы лежат не по порядку и ищутся по ключу, а не по индексу, и «удалить» там — встроенная операция delete, а не собранная из append-ов.

### Песочница: матрица через make

Запустите: make дал два ряда-заготовки, цикл выдал каждому ширину 3, две правки легли в сетку — печать [[0 5 0] [0 0 9]], строк 2, колонок в первой строке 3. Вторая половина: к литералной сетке append добавил целую третью строку — она срез, как и все. Опыты. Первый: поднимите размер make([][]int, 2) до 3 — третья строка после цикла заполнится нулями сама. Второй: поставьте правку matrix[2][0] = 1 ПЕРЕД циклом заполнения — оба движка оборвут программу: строк ещё нет, а запись в несуществующую строку незаконна. Третий: сделайте матрицу рваной — во второй итерации цикла замените make([]int, 3) на make([]int, 2) и уберите правку matrix[1][2], иначе программа оборвётся: у второй строки всего две ячейки.

```go
package main

import "fmt"

func main() {
        matrix := make([][]int, 2)
        for i := range matrix {
                matrix[i] = make([]int, 3)
        }
        matrix[0][1] = 5
        matrix[1][2] = 9
        fmt.Println(matrix, len(matrix), len(matrix[0]))

        grid := [][]int{{1, 2}, {3, 4}}
        grid = append(grid, []int{5, 6})
        fmt.Println(grid, len(grid))
}
```

### Шпаргалка: append, copy и ловушки

| Запись | Что делает | В песочнице |
| --- | --- | --- |
| s = append(s, v) | дописать элемент и присвоить обратно | работает, вывод совпадает в обоих движках |
| var s []int; s = append(s, v) | append к nil-срезу — идиома старта | работает; печать nil-среза ДО append — квирк резерва (<nil>) |
| append(s, a, b, c) | несколько элементов за один вызов | работает |
| append(s, t...) | дописать все элементы другого среза | работает |
| len(s) по шагам роста | рост по одному элементу, амортизированно O(1) | совпадает в обоих движках |
| cap(s) по шагам роста | запас под будущие append | Go удваивает (1 2 4 8), резервный рисует cap = len — неранимо |
| t := s; t = append(t, 9) | дописывание при полном cap | новый массив, сосед цел — совпадает |
| t := s[:2]; t = append(t, 99) | дописывание при запасе cap | Go пишет в общий массив, резервный копирует — неранимо |
| copy(dst, src) | копирование min(len) элементов | работает, включая возвращённое число |
| s = append(s[:i], s[i+1:]...) | удаление с сохранением порядка | работает |
| copy(s[i:], s[i+1:]); s = s[:len(s)-1] | удаление копированием на месте | Go сдвигает элементы, резервный нет — неранимо |
| s[:2:2] / s[low:high:max] | полный срез: cap = max - low | резервный игнорирует третий индекс — неранимо |
| make([]T, 0, n) | пустой срез с запасом | резервный cap 0 — неранимо |
| func f(a int, b int) | функция с несколькими параметрами | резервный парсер не берёт; групповая f(a, b int) работает в обоих |

> **Честно о песочнице: два движка.** Каждый ранимый пример урока прогнан пробником на обоих движках песочницы. Совпадают: append к литералу, к nil-срезу и к пустому литералу, многотомный append и форма append(s, t...), len по шагам роста, присваивание среза при полном cap (сосед цел), copy с правилом минимума и его возврат, append-удаление, матрицы make([][]int, n) и append строки. Расходятся, все случаи проверены наблюдениями: (1) cap после append — настоящий Go прыгает 1 2 4 4 8 8 8 8, резервный всегда рисует cap = len; (2) make([]int, 0, n) — cap n против cap 0; (3) append от вырезки с запасом cap — настоящий Go пишет в общий массив, резервный при срезании копирует и ошибку прячет; (4) copy-удаление на месте — сдвиг есть только в настоящем Go; (5) полный срез s[low:high:max] — резервный игнорирует третий индекс; (6) запись функции f(a int, b int) резервным парсером не принимается — групповая f(a, b int) работает в обоих. Всё перечисленное сознательно вынесено из ранимых блоков.

### Карточки: append, copy и ловушки

- **Что возвращает append и почему результат обязательно присваивать?** — Новый заголовок среза: append не меняет срез на месте. Без s = append(s, v) дописанные элементы остаются за пределами len старого заголовка и пропадают. Отсюда же сигнатура растящих функций: func grow(s []int) []int, на месте вызова — присваивание.
- **Как растёт cap при append в настоящем Go и что показывает резервный движок?** — Ёмкость прыгает примерно удвоением: 1, 2, 4, 8 — между прыжками стоит на месте, амортизированная цена append — O(1). Резервный TS-движок всегда рисует cap равным len; рост ёмкости проверяйте в основном движке или локальном Go.
- **Когда append портит чужую память?** — Когда у окна есть запас cap: append от вырезки s[:2] дописывает в общий массив за пределами len, и оригинал меняется без ошибки. При полном cap (len == cap) append переезжает в новый массив и соседей не задевает.
- **Сколько элементов копирует copy(dst, src) и что возвращает?** — min(len(dst), len(src)); лишнее отбрасывается, хвост приёмника не трогается. Возвращает число скопированных элементов. Независимая копия — пара make(len(s)) + copy, а не t := s, которое делит данные.
- **Как удалить элемент среза с сохранением порядка?** — s = append(s[:i], s[i+1:]...): всё до i плюс всё после i, len уменьшается на 1. Альтернатива — копирование с конца copy(s[i:], s[i+1:]) и укорачивание s = s[:len(s)-1]; в резервном движке она не работает, проверяйте в настоящем Go.
- **Что даёт полный срез s[low:high:max]?** — Ёмкость результата ровно max - low: cap ограничен, append за пределы len переезжает в новый массив и не трогает общую память. max = high — предохранитель от шаринга; резервный движок третий индекс игнорирует.

## Коротко: что запомнить

- append дописывает элементы в конец среза и возвращает новый заголовок: s = append(s, v) — присваивание обязательно, иначе дописанное пропадает.
- append к nil-срезу законен и идиоматичен: var s []int; s = append(s, ...) — make не нужен, старт с нуля — норма.
- len растёт по единице, cap — скачками примерно удвоением в настоящем Go (1, 2, 4, 8); амортизированная цена append — O(1) на элемент.
- Подводный камень срезов: append от вырезки с запасом cap пишет в общий массив и портит оригинал; при полном cap append переезжает и сосед цел.
- Функция правит элементы снаружи, но рост виден только через возврат: идиома s = append(s, ...) — функция возвращает срез, вызов присваивает.
- copy(dst, src) копирует min(len) элементов и возвращает их число; независимая копия — пара make + copy, а не t := s.
- Удаление с сохранением порядка — s = append(s[:i], s[i+1:]...); полный срез s[low:high:max] ставит cap-поводок против шаринга; матрицы растут из make([][]int, rows).

## Домашнее задание

Соберите журнал покупок: начните с var log []string и допишите append-ом одним вызовом четыре покупки — хлеб, молоко, сыр, кофе. Удалите «молоко» append-формой с сохранением порядка — должно напечататься [хлеб сыр кофе]. Затем сделайте независимый отчёт: report := make([]string, len(log)) и copy(report, log), поменяйте report[0] на «багет» и убедитесь, что log не дрогнул — в отличие от t := log. Финал: матрица цен 2×2 через make([][]int, 2) с заполнением циклом и append третьей строки с двумя числами. Все приёмы — из этого урока; [оглавление курса](/courses/go-basics/) под рукой.

## Вопросы и ответы по уроку

### Почему нельзя просто append(s, v) без присваивания?

append возвращает новый заголовок среза и не меняет старый: без присваивания результат теряется, len остаётся прежним. Компилятор такой вызов пропускает — вызов функции законен, поэтому идиома s = append(s, ...) — это правило, а не синтаксис. Отсюда и сигнатуры растящих функций: func grow(s []int) []int.

### Чем copy лучше ручного цикла при копировании среза?

copy сам ограничивает по минимуму длин и не падает, если источник длиннее приёмника или наоборот; цикл требует следить за границами и писать строго по индексу. На малых срезах разницы в скорости нет — выбирайте читаемость. Важнее другое: независимую копию даёт пара make(len(s)) + copy, а t := s делит данные с оригиналом.

### Почему append иногда меняет исходный срез, а иногда нет?

Всё решает запас ёмкости. Если len == cap, append выделяет новый массив — оригинал цел. Если запас есть (после вырезки s[:2] или make с тремя аргументами), элемент вписывается в общий массив — оригинал меняется без ошибки. Предохранитель — полный срез s[low:high:max] с max = high: append в этом случае всегда переезжает.
