Срезы под микроскопом: capacity, copy, grow и ловушки в Go
Разбираем срезы в Go под капотом: как они устроены, почему capacity решает, чем опасны общие массивы и как избежать скрытых багов.
Срезы в Go — один из самых мощных, но и коварных инструментов языка. С первого взгляда они кажутся просто «динамическими массивами», но под капотом у них скрыто достаточно нюансов, чтобы одна строчка кода превратилась в баг, отладка которого займёт несколько часов. Давайте разберёмся, как они работают на самом деле.

Из чего состоит срез
Срез в Go — это не массив. Это «тонкая обёртка», которая хранит:
Указатель на массив — реальное место хранения данных.
Длину (len) — сколько элементов доступно прямо сейчас.
Ёмкость (cap) — сколько элементов можно вместить, не перевыделяя память.
s := []int{1, 2, 3, 4}
fmt.Println(len(s)) // 4
fmt.Println(cap(s)) // 4Capacity и его роль
Capacity — это запас. Когда мы делаем append, Go проверяет:
Если хватает capacity — новые элементы просто записываются в тот же массив.
Если capacity кончилось — Go создаёт новый массив большего размера и копирует туда старые данные.
s := make([]int, 2, 4) // len=2, cap=4
s = append(s, 10, 20) // места ещё хватает
fmt.Println(cap(s)) // 4
s = append(s, 30) // cap кончился → новый массив
fmt.Println(cap(s)) // 8Copy и подводные камни
Функция copy делает поверхностное копирование:
a := []int{1, 2, 3}
b := make([]int, len(a))
copy(b, a)
b[0] = 99
fmt.Println(a) // [1 2 3]
fmt.Println(b) // [99 2 3]Но если просто присвоить:
a := []int{1, 2, 3}
b := a
b[0] = 99
fmt.Println(a) // [99 2 3] ❗Тут a и b указывают на один и тот же массив — частая ошибка новичков.

Grow: как растут срезы
Go не гарантирует точный алгоритм роста, но обычно действует так:
Если capacity маленький — удваивает.
Если capacity уже большой — увеличивает примерно на 25%.
👉 Поэтому при работе с большими данными лучше заранее выделять make([]T, 0, N) с нужным запасом.
Pitfalls: где чаще всего горят разработчики
Общий underlying array
a := []int{1, 2, 3, 4, 5} b := a[:3] c := a[2:] c[0] = 99 fmt.Println(b) // [1 2 99] 😱bиcиспользуют один и тот же массив.Рост ломает «связь»
a := []int{1, 2, 3} b := a a = append(a, 4) // cap исчерпался → новый массив b[0] = 99 fmt.Println(a) // [1 2 3 4] fmt.Println(b) // [99 2 3]После роста у
aуже другой массив, и связь сbпотерялась.Срезы большого массива
big := make([]byte, 1e6) small := big[:1] // small выглядит крошечным, но держит в памяти весь мегабайтРешение:
copyв новый срез.
Итог
Срезы в Go — удобный инструмент, но:
помните про capacity и рост,
используйте
copy, если хотите независимые данные,осторожнее со «срезами от срезов».
Понимание устройства срезов под капотом избавит вас от неожиданных багов и поможет писать более надёжный код.