В чём ключевое отличие слайса (среза) от массива?
В языке 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 и т.д.
-