Срезы под микроскопом: capacity, copy, grow и ловушки в Go

Разбираем срезы в Go под капотом: как они устроены, почему capacity решает, чем опасны общие массивы и как избежать скрытых багов.

Разработка

6 мин

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

Из чего состоит срез

Срез в Go — это не массив. Это «тонкая обёртка», которая хранит:

  • Указатель на массив — реальное место хранения данных.

  • Длину (len) — сколько элементов доступно прямо сейчас.

  • Ёмкость (cap) — сколько элементов можно вместить, не перевыделяя память.

 s := []int{1, 2, 3, 4}
 fmt.Println(len(s)) // 4
 fmt.Println(cap(s)) // 4

Capacity и его роль

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))    // 8

Copy и подводные камни

Функция 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: где чаще всего горят разработчики

  1. Общий 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 используют один и тот же массив.

  2. Рост ломает «связь»

     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 потерялась.

  3. Срезы большого массива

     big := make([]byte, 1e6)
     small := big[:1]
     // small выглядит крошечным, но держит в памяти весь мегабайт

    Решение: copy в новый срез.

Итог

Срезы в Go — удобный инструмент, но:

  • помните про capacity и рост,

  • используйте copy, если хотите независимые данные,

  • осторожнее со «срезами от срезов».

Понимание устройства срезов под капотом избавит вас от неожиданных багов и поможет писать более надёжный код.

Комментарии