Срез — это окно на массив: указатель, длина и ёмкость. От этого зависят append, общие подсрезы и то, что видно снаружи после вызова функции.
Типы и память Go golang собеседование
Срез в Go выглядит как список, который умеет расти. На самом деле это маленькая структура из трёх полей: указатель на массив, длина len и ёмкость cap. Сам срез при передаче копируется. Копируется заголовок, не элементы. Массив за указателем общий, пока append не выделит новый.
В бэкенде так устроены почти все списки: строки из БД, тело HTTP, очередь задач. Два частых бага из одной модели. Функция добавила элемент — снаружи его нет. Подсрез поменял исходный слайс — «испортили чужие данные».
Аналогия: массив — улица домов, срез — окно, через которое видно несколько домов подряд. Два окна могут смотреть на одну улицу. Покрасили дом через одно окно — через второе виден тот же цвет.
Как устроен срез
Поле ptr указывает на первый видимый элемент. len — сколько элементов можно читать и писать по индексу. cap — сколько ещё влезает в тот же массив без новой аллокации. Запись s[i] = x меняет ячейку массива. Это видно всем срезам с тем же ptr.
Пример. Слайс как окно на массив: a := []int{1, 2, 3, 4}, затем b := a[:2]. У b длина 2, ёмкость 4, указатель тот же. b[0] = 9 меняет и a[0].
type sliceHeader struct {
ptr *byte
len int
cap int
}
func demo() {
a := []int{1, 2, 3, 4} // len=4, cap=4
b := a[:2] // ptr тот же, len=2, cap=4
b = append(b, 9) // cap хватает: пишем в тот же массив
// a теперь {1, 2, 9, 4} — общий массив
}
Заголовок среза копируется. Данные лежат в массиве за указателем.
append всегда возвращает новый заголовок. Если len < cap, он пишет в тот же массив и увеличивает len. Если len == cap, рантайм выделяет новый массив, копирует элементы и возвращает заголовок уже на него. Обычно новый массив берут с запасом, чтобы не копировать на каждый append. В среднем это дёшево: точные копии редки, зато иногда копируют все n элементов сразу.
Пример. Был слайс длины 3 и ёмкости 3. Сделали s = append(s, 4). Места нет — появился новый массив. Старый живёт, пока на него смотрит другой срез.
Передача в функцию
Функция получает копию заголовка. Запись s[i] = x идёт в общий массив — вызывающий увидит изменение. append внутри функции меняет только локальную копию заголовка. Снаружи len и ptr не обновятся, пока вы не вернёте срез и не присвоите его.
func set(s []int) {
s[0] = 9 // видно вызывающему: тот же ptr
}
func appendLocal(s []int) {
s = append(s, 4) // новый header уходит в мусор
}
func appendReturn(s []int) []int {
return append(s, 4)
}
func main() {
a := []int{1, 2, 3}
set(a) // a[0] == 9
appendLocal(a) // четвёртого элемента нет
a = appendReturn(a) // так правильно
}
Индекс меняет массив на месте. append без возврата теряет новый заголовок.
Пример. Как с записной книгой: коллеге отдали ксерокопию обложки с адресом той же страницы. Он может дописать в клетку. Если страница кончилась и он купил новую тетрадь, ваша обложка всё ещё смотрит на старую.
Типичная ошибка
Вызвать append внутри функции и не вернуть результат, думая «срез же ссылка». Снаружи останется старый len. После выделения нового массива снаружи останется и старый массив. Правило то же, что в одном блоке: всегда s = append(s, x).
Подсрезы
Выражение b := a[1:3] не копирует элементы. У b свой len. Ёмкость часто тянется до конца исходного массива. Запись в b[0] меняет a[1]. Если потом сделать append(b, x) при запасе cap, можно затереть хвост a, который «как будто не входил» в подсрез. Чтобы отвязаться, копируют: append([]T(nil), a...) или slices.Clone.
Пример. HTTP-хендлер отдал наружу buf[:n] из внутреннего буфера. Получатель дописал в этот подсрез — испортил буфер, который потом вернут в sync.Pool. Безопаснее скопировать то, что переживает текущий вызов.
range по срезу копирует элемент в переменную цикла. Менять v в for _, v := range s бесполезно для массива. Нужен индекс или указатель.
На собеседовании
Нарисуйте ptr, len и cap. Копируется заголовок, не данные. Дальше развилка append: тот же массив или новый. Подсрез смотрит на тот же массив, пока его явно не клонировали. Функция, которая растит срез, должна его вернуть.