В чём ключевое отличие слайса (среза) от массива?


В языке Go массивы (arrays) и срезы (slices) — это два тесно связанных, но фундаментально различных типа данных. Они оба представляют собой последовательность элементов одного типа, но их поведение, семантика хранения, передача в функции и назначение отличаются. Ключевое различие заключается в размере и гибкости: массив — это фиксированная структура, срез — динамическая.

1. Определение массива и среза

Массив (array)

Массив в Go имеет фиксированную длину, заданную во время компиляции. Это значимый тип, его размер входит в сигнатуру типа.

var a \[3\]int = \[3\]int{1, 2, 3}

Здесь a — это массив из трёх целых чисел. Тип: [3]int.

Срез (slice)

Срез — это абстракция над массивом, которая предоставляет удобный интерфейс к его подмножеству. Он может изменять длину и автоматически расширяться.

s := \[\]int{1, 2, 3}

Тип: []int — срез из int.

2. Фиксированность размера

Массив:

  • Размер входит в тип.

  • Размер нельзя изменить.

  • [3]int и [4]int — разные типы.

func f(a \[3\]int) {} // работает
func f(a \[4\]int) {} // другой тип

Срез:

  • Размер не фиксирован.

  • Массив можно разрезать: slice := a[0:2].

  • Тип не зависит от длины: []int.

func f(s []int) {} // подходит для срезов любой длины

3. Передача в функции (значение vs. ссылка)

Массив:

  • Передаётся по значению, то есть копируется.

  • Изменения внутри функции не влияют на оригинал.

func changeArray(arr \[3\]int) {
arr\[0\] = 999
}

Срез:

  • Передаётся по значению, но содержит ссылку на массив.

  • Изменения элементов отражаются на оригинале.

func changeSlice(s \[\]int) {
s\[0\] = 999
}

То есть срез — это объект, указывающий на массив, но сам по себе срез передаётся по значению.

4. Структура данных: что внутри

Массив:

Простая линейная структура: не содержит никаких дополнительных метаданных.

\[1, 2, 3\] // данные

Срез:

Внутренне представлен структурой:

type slice struct {
pointer \*T // указатель на первый элемент
length int // текущая длина
capacity int // ёмкость (макс. доступная длина)
}

Это объясняет, почему срез можно расширять (до capacity) и изменять его размер.

5. Создание среза из массива

Можно получить срез из массива:

a := \[4\]int{10, 20, 30, 40}
s := a\[1:3\] // \[20 30\]

Здесь s ссылается на сегмент массива a, а изменения через s повлияют на a.

6. Гибкость и использование

Массив:

  • Используется редко.

  • Полезен, если нужно гарантировать фиксированную длину (например, координаты [3]float64).

Срез:

  • Основной инструмент работы с коллекциями.

  • Удобен для динамических структур.

s := make(\[\]int, 0, 10) // длина 0, capacity 10
s = append(s, 5) // динамически увеличивается

7. Длина и ёмкость

Массив:

  • Длина == capacity.

  • Нет смысла измерять ёмкость отдельно.

Срез:

  • len(slice) — длина (кол-во элементов).

  • cap(slice) — ёмкость (от начала до конца связанного массива).

a := \[5\]int{1, 2, 3, 4, 5}
s := a\[1:3\] // \[2, 3\]
fmt.Println(len(s)) // 2
fmt.Println(cap(s)) // 4 (от a\[1\] до a\[4\])

8. Изменяемость и безопасное расширение

Срез может быть расширен, если его capacity это позволяет:

s := \[\]int{1, 2, 3}
s = append(s, 4, 5)

Если append выходит за пределы текущего cap, Go создаёт новый массив, копирует данные и возвращает новый срез.

9. Копирование и независимость

a := \[3\]int{1, 2, 3}
b := a // копия массива
b\[0\] = 999 // a не изменится
s1 := \[\]int{1, 2, 3}
s2 := s1 // копия среза (по значению, но указывает на те же данные)
s2\[0\] = 999 // s1 тоже изменится

Чтобы сделать независимую копию среза:

s1 := \[\]int{1, 2, 3}
s2 := make(\[\]int, len(s1))
copy(s2, s1)

10. Инициализация значений

Массив:

var a \[3\]int // \[0 0 0\]
a := \[3\]int{1, 2, 3}
a := \[...\]int{1, 2, 3} // компилятор определяет длину

Срез:

var s \[\]int // nil slice
s := \[\]int{1, 2, 3} // инициализированный
s := make(\[\]int, 3) // \[0 0 0\]

11. nil и zero значения

Массив:

var a \[3\]int
a == \[3\]int{0, 0, 0} // true

Нельзя сравнить массивы с разной длиной.

Срез:

var s \[\]int
fmt.Println(s == nil) // true
fmt.Println(len(s)) // 0

Важно: nil срез и []int{} — разные значения, но функционально почти эквивалентны.

12. Работа с диапазонами

Массив:

for i, v := range a {
fmt.Println(i, v)
}

Срез:

for i, v := range s {
fmt.Println(i, v)
}

Идентичная работа, но срезы более удобны для передачи в функции, так как могут быть любого размера.

13. Вывод типов и литералы

a := \[3\]string{"a", "b", "c"} // массив
s := \[\]string{"a", "b", "c"} // срез

Оба можно итерировать, но s можно динамически расширить, a — нет.

14. Сравнение

Массив:

  • Поддерживает == (если элементы поддерживают сравнение).

  • [3]int{1, 2, 3} == [3]int{1, 2, 3} → true

Срез:

  • Сравнивать срезы оператором == нельзя, кроме как с nil.

  • Для сравнения по значениям — нужно использовать цикл или функцию reflect.DeepEqual.

15. Использование в интерфейсах

Массивы — редко используются в интерфейсах, т.к. их тип жёстко зависит от длины. Срезы — универсальны:

func sum(nums \[\]int) int

Вызывается с любым срезом, включая a[:], где a — массив.

16. Примеры под капотом

a := \[5\]int{10, 20, 30, 40, 50}
s := a\[1:4\] // \[20, 30, 40\]
  • s указывает на a[1]

  • len(s) = 3

  • cap(s) = 4 (до a[5])

  • изменение s[0] = 999 меняет a[1]

17. Когда использовать массивы

  • В редких случаях, когда:

    • нужна **гарантированная длина
      **
    • работа с низкоуровневыми данными (например, сетевые пакеты фиксированной длины)

    • требуется **максимальная производительность без аллокаций
      **

    • вы хотите использовать массив как значение (например, ключ в map)

18. Когда использовать срезы

  • Почти всегда в обычной Go-разработке:

    • передача коллекций в функции

    • работа с динамическими структурами

    • сортировка, фильтрация, агрегация

    • приём данных из append, make, copy, range и т.д.