Потому что на самом деле append быстро выводит тебя на понимание трёх базовых вещей в Go:
slice — это не массив
slice имеет длину и вместимость
за slice может стоять общий underlying array
Именно из-за этого append иногда ведёт себя нормально и ожидаемо, а иногда будто ломает мозг.
Практический вывод
Если ты не понял append, значит ты ещё не до конца понял slice. А если не понял slice, дальше начнут сыпаться и copy, и range, и работа с []byte, и куски конкурентного кода.
К строке append напрямую не применяют как к строковому типу.
Но есть особый случай в языке: если у тебя []byte, туда можно аппендить строку через ... как байты.
b := []byte{"G", "o"}
_ = b
Корректный практический пример:
b := []byte{'G', 'o'}
b = append(b, "lang"...)
fmt.Println(string(b))
Результат:
Golang
Это специальная форма для []byte + string.
Короткая запоминалка
• append работает со slice •array надо сначала превратить в slice •map не аппендят, но можно аппендить slice внутри map •string напрямую не аппендят, но можно аппендить её байты в []byte
len, cap и underlying array: без этого append не понять
Вот теперь начинается главное.
Если смотреть на slice просто как на “массив, который можно расширять”, тема append будет постоянно вызывать путаницу.
В Go slice — это не сам массив, а небольшая структура-описатель над массивом.
Удобная модель такая:
pointer на underlying array;
len — сколько элементов сейчас видно;
cap — сколько элементов вообще можно вместить до перераспределения.
Что такое len
len(slice) — это текущая длина slice, то есть сколько элементов в нём сейчас доступно.
s := []int{10, 20, 30}
fmt.Println(len(s)) // 3
Что такое cap
cap(slice) — это вместимость, то есть сколько элементов slice может вместить до того, как append придётся искать новую память.
Потому что поведение append зависит именно от cap. Если места хватает, append просто дописывает новый элемент в уже существующий underlying array. Если места не хватает, Go создаёт новый массив, копирует туда старые данные и уже туда добавляет новый элемент.
Пример с make
s := make([]int, 0, 3)
fmt.Println(len(s), cap(s))
s = append(s, 1, 2, 3)
fmt.Println(s, len(s), cap(s))
Здесь у slice была вместимость 3, поэтому всё поместилось без перераспределения.
А если добавить ещё один элемент:
s = append(s, 4)
fmt.Println(s, len(s), cap(s))
Вот теперь Go уже, скорее всего, выделит новый underlying array.
Почему скорее всего
Потому что конкретная формула роста capacity — это деталь реализации runtime, и она менялась между версиями Go. На практике важно не заучивать “всегда удваивается”, а понимать сам принцип:
Если cap не хватает, append может выделить новую память
Думай о slice как о виде на полку.
len — сколько книг сейчас стоит на виду
cap — сколько ещё мест на этой полке есть до переезда
append — попытка поставить ещё одну книгу
если полка кончилась, книги переезжают на новую
Когда append использует старую память, а когда создаёт новую
Сценарий 1. Места хватает
Если у slice хватает capacity, append использует тот же underlying array.
Если capacity закончилась, append создаёт новый underlying array.
s := []int{1, 2, 3}
t := append(s, 4)
Так как у s длина и вместимость часто совпадают, очень вероятно, что t уже живёт на новой памяти.
Теперь изменение t уже не обязательно затронет s.
Почему это так важно
Потому что отсюда растёт половина багов по slice в Go:
человек думает, что append создал полностью независимую копию, а память всё ещё общая
или наоборот думает, что всё меняется вместе, а append уже ушёл в новый массив
Правило
Append может как оставить slice на старой памяти, так и перевести его на новую. Если тебе критично нужен отдельный независимый кусок данных, лучше явно сделать копию, а не надеяться на побочный эффект роста capacity.
Когда это всплывает в реальной работе
Очень часто в таких местах:
helper-функции, которые принимают slice и что-то в него append-ят
построение буферов
работа с подslice
удаление и вставка элементов
переиспользование временных списков в цикле
Почему результат append нужно сохранять
Неправильно
s := []int{1, 2, 3}
append(s, 4)
fmt.Println(s)
Многие ожидают увидеть: [1 2 3 4]
Но это неверное ожидание.
Почему так нельзя
Потому что append возвращает результат.
Go прямо говорит: вот тебе обновлённый slice, работай уже с ним.
Правильно
s := []int{1, 2, 3}
s = append(s, 4)
fmt.Println(s)
Почему язык устроен именно так
Потому что append может вернуть slice:
либо на старом массиве
либо уже на новом
Если бы язык делал вид, что ничего возвращать не надо, ты бы не знал, какой именно slice теперь актуален.
Либо, если нужен совсем другой уровень управления, уже работать через указатель на slice. Но для новичка 99% времени правильный путь — просто возвращать новый slice.
Append slice к slice, append nil slice, append в начало и удаление через append
Теперь разберём прикладные паттерны, которые встречаются чаще всего.
Как append один slice к другому
Для этого нужен оператор ..., который разворачивает второй slice в список аргументов.
Потому что часто можно не делать лишнюю инициализацию заранее. Например, map со значениями-слайсами хорошо сочетается с этим паттерном:
m := map[string][]string{}
m["go"] = append(m["go"], "slice", "append")
Если ключа ещё не было, m["go"] даст nil slice, а append спокойно создаст рабочий slice.
Как добавить элемент в начало slice
Стандартного prepend в Go нет, но можно сделать так:
s := []int{1, 2, 3}
s = append([]int{0}, s...)
fmt.Println(s)
Результат:
[0 1 2 3]
Работает, но надо понимать: это обычно дороже, чем append в конец, потому что требует новый результат и копирование.
Как удалить элемент через append
Очень популярный паттерн:
s := []int{10, 20, 30, 40, 50}
i := 2
s = append(s[:i], s[i+1:]...)
fmt.Println(s)
Результат:
[10 20 40 50]
Что здесь происходит
Мы берём:
кусок до удаляемого индекса
кусок после него
склеиваем их через append
Важный нюанс
Этот способ может переиспользовать тот же underlying array. Иногда это нормально. Но если ты работаешь с большими объектами, долгоживущими ссылками или чувствительными данными, надо понимать, что старые элементы могут ещё оставаться в памяти underlying array до перезаписи.
Append строковых значений
Если у тебя slice строк, всё обычным образом работает:
Практика: 7 коротких упражнений, чтобы append реально закрепился
Упражнение 1. Один элемент
Что тренируем: базовую механику append.
Задача: Создай slice из трёх чисел и добавь в конец четвёртое.
Упражнение 2. Несколько элементов
Что тренируем: variadic append.
Задача: Добавь к slice сразу три новых числа одним вызовом.
Упражнение 3. Append slice к slice
Что тренируем: оператор ....
Задача: Объедини два slice в один.
Упражнение 4. Nil slice
Что тренируем: работу append с nil slice.
Задача: Объяви var s []int, затем через append добавь в него элементы.
Упражнение 5. len и cap
Что тренируем: понимание роста slice.
Задача: Создай slice через make([]int, 0, 2) и по одному добавляй элементы, печатая len и cap после каждого append.
Упражнение 6. Общая память
Что тренируем: понимание shared underlying array.
Задача: Сделай slice с запасом capacity, append-ни к нему новый slice и проверь, меняются ли исходные данные после изменения результата.
Упражнение 7. Удаление через append
Что тренируем: практический шаблон удаления.
Задача: Удали элемент из середины slice через append(slice[:i], slice[i+1:]...).
Мини-шаблоны, которые стоит унести в работу
Базовый append:
s = append(s, x)
Несколько элементов:
s = append(s, a, b, c)
Slice к slice:
s1 = append(s1, s2...)
Nil slice:
var s []int
s = append(s, 1, 2, 3)
Prepend:
s = append([]int{0}, s...)
Удаление элемента:
s = append(s[:i], s[i+1:]...)
Как самому проверить, что тема реально улеглась
После практики попробуй без подсказки ответить:
Почему append возвращает результат?
Когда append оставляет старую память, а когда создаёт новую?
Чем len отличается от cap?
Почему append(s, x) без присваивания — ошибка мышления?
Зачем нужен ... при append slice к slice?
Почему append к nil slice работает нормально?
Почему array напрямую append-ить нельзя?
Если на всё это отвечаешь спокойно, значит база по теме уже реально села.
Зачем понимать append, если это кажется маленькой функцией
append — отличный пример маленькой темы, которая на деле очень хорошо показывает качество базы.
Если человек не до конца понимает:
что такое slice
чем slice отличается от array
как работают len и cap
что такое underlying array
почему память бывает общей
То это очень быстро всплывает и в рабочем коде, и на собеседовании.
В реальности важна плотная база и понимание того, что происходит в памяти, а не только на уровне синтаксиса. Именно поэтому тема append маленькая по объёму, но очень важная по смыслу.
Senior Go developer
Работал в Авито в инфраструктуре
Кодил на Go, Java, Python, JS
200+ собеседований провел лично
Менторю больше 2 лет
У меня большой нетворк: всегда в курсе, как проходит найм в разных компаниях