Лекция 5. Массивы и slices
Цели лекции
После лекции студент должен:
- различать массив и slice;
- объявлять и инициализировать массивы;
- создавать slices через литерал, срез массива и
make; - понимать
lenиcap; - безопасно применять
appendи сохранять его результат; - понимать совместное базовое хранилище slices;
- копировать данные через
copyиslices.Clone; - отличать
nilslice от пустого; - передавать slices в функции;
- избегать ошибок границ и случайного изменения общих данных.
1. Зачем нужны последовательности
Программы редко работают с одним значением. Обычно требуется хранить список оценок, задач, пользователей или измерений.
В Go есть два тесно связанных типа:
- array — массив фиксированной длины;
- slice — представление последовательности переменной длины поверх массива.
В прикладном коде slices встречаются значительно чаще, но понимание массива необходимо для понимания их устройства.
2. Массив
Тип массива включает длину:
var scores [5]intscores содержит ровно пять элементов типа int. После объявления элементы
получают zero value:
fmt.Println(scores) // [0 0 0 0 0]Обращение по индексу:
scores[0] = 90scores[1] = 82
fmt.Println(scores[0])fmt.Println(scores[1])Индексация начинается с нуля. Допустимые индексы массива длины 5 — от 0 до 4.
3. Литералы массивов
days := [7]string{ "Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота", "Воскресенье",}Компилятор может определить длину:
values := [...]int{10, 20, 30}fmt.Printf("%T\n", values) // [3]intМожно задавать отдельные индексы:
flags := [5]bool{ 0: true, 3: true,}Получится [true false false true false].
4. Длина является частью типа
[3]int и [4]int — разные типы:
first := [3]int{1, 2, 3}second := [4]int{1, 2, 3, 4}
// first = second // ошибка: несовместимые типыПоэтому функции с параметром [3]int принимают только массивы длины 3:
func sumThree(values [3]int) int { return values[0] + values[1] + values[2]}Для произвольной длины обычно принимают slice.
5. Массив передаётся по значению
Передача массива копирует все элементы:
func changeFirst(values [3]int) { values[0] = 100}
func main() { values := [3]int{1, 2, 3} changeFirst(values) fmt.Println(values) // [1 2 3]}Для большого массива копирование может быть дорогим. Можно передать указатель, но в большинстве прикладных задач используют slice.
6. Сравнение массивов
Массивы сравнимы через ==, если сравним тип элемента:
first := [3]int{1, 2, 3}second := [3]int{1, 2, 3}
fmt.Println(first == second) // trueМассивы разной длины нельзя сравнить напрямую, потому что у них разные типы.
7. Что такое slice
Slice не хранит элементы непосредственно. Концептуально он содержит:
- ссылку на базовый массив;
- длину
len; - вместимость
cap.
Тип slice не содержит длину:
var names []stringSlice можно создать литералом:
names := []string{"Анна", "Борис", "Виктор"}Обратите внимание: у массива между скобками находится число, у slice скобки пустые.
array := [3]int{1, 2, 3}slice := []int{1, 2, 3}8. Получение slice из массива
numbers := [5]int{10, 20, 30, 40, 50}part := numbers[1:4]
fmt.Println(part) // [20 30 40]Левая граница включается, правая не включается:
[from:to] → from, from+1, ..., to-1Сокращённые формы:
numbers[:3] // первые три элементаnumbers[2:] // от индекса 2 до концаnumbers[:] // все элементы9. Длина и вместимость
numbers := [5]int{10, 20, 30, 40, 50}part := numbers[1:3]
fmt.Println(len(part)) // 2fmt.Println(cap(part)) // 4Длина — число доступных элементов. Вместимость — число элементов от начала slice до конца базового массива.
Доступ по индексу ограничен длиной, а не вместимостью:
// fmt.Println(part[2]) // panic: индекс равен len(part)Вместимость показывает, насколько slice может вырасти через append без
выделения нового базового массива.
10. Создание через make
make создаёт slice указанной длины:
values := make([]int, 5)fmt.Println(values) // [0 0 0 0 0]fmt.Println(len(values)) // 5fmt.Println(cap(values)) // 5Можно отдельно задать вместимость:
values := make([]int, 0, 10)fmt.Println(len(values)) // 0fmt.Println(cap(values)) // 10Второй вариант удобен, если итоговое количество элементов приблизительно известно. Это уменьшает число перераспределений памяти.
Ошибочная идея:
values := make([]int, 5)values = append(values, 10)fmt.Println(values) // [0 0 0 0 0 10]Если slice должен наполняться через append, начальная длина обычно равна нулю.
11. Добавление через append
numbers := []int{10, 20}numbers = append(numbers, 30)numbers = append(numbers, 40, 50)append возвращает новый slice. Результат необходимо сохранить:
numbers = append(numbers, 30)Плохой вариант:
append(numbers, 30) // результат потерян; такой вызов не компилируется как инструкцияДобавление другого slice:
first := []int{1, 2}second := []int{3, 4}first = append(first, second...)12. Перевыделение памяти
Если вместимости недостаточно, append выделяет новый массив, копирует элементы
и возвращает slice, указывающий уже на новое хранилище.
numbers := make([]int, 0, 2)
for value := 1; value <= 5; value++ { numbers = append(numbers, value) fmt.Printf("len=%d cap=%d values=%v\n", len(numbers), cap(numbers), numbers)}Точная стратегия роста вместимости является деталью реализации. Нельзя писать
логику, зависящую от конкретного коэффициента роста cap.
13. Общее базовое хранилище
Несколько slices могут указывать на один массив:
numbers := []int{10, 20, 30, 40}left := numbers[:2]middle := numbers[1:3]
middle[0] = 999
fmt.Println(numbers) // [10 999 30 40]fmt.Println(left) // [10 999]Присваивание slice не копирует элементы:
copyOfHeader := numberscopyOfHeader[0] = 777fmt.Println(numbers[0]) // 777Копируется только небольшое описание slice, а базовое хранилище остаётся общим.
14. Неочевидный append к подсрезу
numbers := []int{10, 20, 30, 40}part := numbers[:2]part = append(part, 999)
fmt.Println(numbers) // [10 20 999 40]У part оставалась вместимость в исходном массиве, поэтому append перезаписал
следующий элемент.
Если slice должен быть изолирован, его копируют или ограничивают вместимость.
Полное выражение среза:
part := numbers[:2:2]part = append(part, 999)Третья граница ограничивает вместимость. Здесь append будет вынужден выделить
новый массив.
15. Копирование через copy
source := []int{10, 20, 30}destination := make([]int, len(source))
copied := copy(destination, source)
fmt.Println(copied) // 3fmt.Println(destination) // [10 20 30]copy возвращает количество скопированных элементов — минимум длин источника и
назначения.
source := []int{1, 2, 3, 4}destination := make([]int, 2)
fmt.Println(copy(destination, source)) // 2fmt.Println(destination) // [1 2]16. Клонирование
Пакет slices предоставляет удобную функцию:
import "slices"
original := []int{1, 2, 3}clone := slices.Clone(original)
clone[0] = 100
fmt.Println(original) // [1 2 3]fmt.Println(clone) // [100 2 3]Альтернативная идиома без пакета:
clone := append([]int(nil), original...)slices.Clone обычно яснее выражает намерение.
Клонирование поверхностное. Если элементы содержат pointers, slices или maps, вложенные данные могут остаться общими.
17. nil slice и пустой slice
var first []intsecond := []int{}third := make([]int, 0)У всех длина равна нулю:
fmt.Println(len(first), len(second), len(third)) // 0 0 0Только первый равен nil:
fmt.Println(first == nil) // truefmt.Println(second == nil) // falseappend работает с nil slice:
first = append(first, 10)В большинстве вычислений nil и пустой slice ведут себя одинаково. Различие
может быть видно при сериализации или взаимодействии с API. Функции обычно
должны корректно принимать оба варианта.
18. Сравнение slices
Slices нельзя сравнивать через ==, кроме сравнения с nil:
first := []int{1, 2, 3}second := []int{1, 2, 3}
// fmt.Println(first == second) // ошибка компиляцииДля сравнения элементов используется slices.Equal:
fmt.Println(slices.Equal(first, second)) // true19. Перебор slice
Индекс и значение:
for index, value := range numbers { fmt.Println(index, value)}Только значение:
for _, value := range numbers { fmt.Println(value)}Только индекс:
for index := range numbers { numbers[index] *= 2}Переменная value получает копию элемента. Изменение её самой не изменяет
slice:
for _, value := range numbers { value *= 2}Для изменения элементов используют индекс.
20. Удаление элемента
С сохранением порядка:
index := 2numbers = append(numbers[:index], numbers[index+1:]...)Элементы после удалённого сдвигаются. Чтобы старая ссылка не удерживалась в хранилище slice ссылочных значений, последний элемент можно обнулить перед уменьшением длины.
В современном Go пакет slices предлагает:
numbers = slices.Delete(numbers, index, index+1)Диапазон удаления полуоткрытый: [from, to).
Если порядок не важен, элемент можно заменить последним:
numbers[index] = numbers[len(numbers)-1]numbers = numbers[:len(numbers)-1]Это быстрее, но изменяет порядок.
21. Вставка элемента
Пакет slices:
numbers = slices.Insert(numbers, 2, 100, 200)Значения вставляются перед индексом 2.
Без пакета операция требует аккуратно увеличить slice и сдвинуть хвост. Для прикладного кода стандартная функция обычно понятнее.
22. Сортировка и поиск
numbers := []int{40, 10, 30, 20}slices.Sort(numbers)fmt.Println(numbers) // [10 20 30 40]slices.Sort изменяет исходный slice. Если исходный порядок нужен, сначала
клонируйте данные:
sorted := slices.Clone(numbers)slices.Sort(sorted)Проверка наличия:
fmt.Println(slices.Contains(numbers, 30))Поиск индекса:
fmt.Println(slices.Index(numbers, 30))Если значения нет, slices.Index возвращает -1.
23. Передача slice в функцию
func doubleAll(values []int) { for index := range values { values[index] *= 2 }}Изменение существующих элементов видно вызывающей стороне:
numbers := []int{1, 2, 3}doubleAll(numbers)fmt.Println(numbers) // [2 4 6]Но изменение длины локальной копии описателя не возвращается автоматически:
func addValue(values []int, value int) { values = append(values, value)}Правильный вариант возвращает новый slice:
func addValue(values []int, value int) []int { return append(values, value)}
numbers = addValue(numbers, 4)24. Фильтрация
Создание нового результата:
func positive(values []int) []int { result := make([]int, 0, len(values))
for _, value := range values { if value > 0 { result = append(result, value) } }
return result}Вместимость заранее равна максимально возможному размеру. Длина остаётся нулевой до фактического добавления элементов.
Фильтрация на месте:
func positiveInPlace(values []int) []int { result := values[:0]
for _, value := range values { if value > 0 { result = append(result, value) } }
return result}Второй вариант экономит выделение памяти, но изменяет общее базовое хранилище. Его следует использовать только при ясном владении данными.
25. Многомерные slices
matrix := [][]int{ {1, 2, 3}, {4, 5, 6}, {7, 8, 9},}
fmt.Println(matrix[1][2]) // 6Строки могут иметь разную длину:
triangle := [][]int{ {1}, {2, 3}, {4, 5, 6},}При создании через make каждую строку нужно создать отдельно:
matrix := make([][]int, 3)for row := range matrix { matrix[row] = make([]int, 4)}26. Удержание большого массива
Маленький slice может удерживать в памяти весь большой базовый массив:
large := make([]byte, 100_000_000)small := large[:10]Пока small используется, сборщик мусора не может освободить массив. Если нужно
оставить только небольшой фрагмент надолго, скопируйте его:
small := slices.Clone(large[:10])Это важно для серверов и длительно работающих процессов.
27. Практический пример: статистика оценок
package main
import ( "fmt" "slices")
func validScores(scores []int) []int { result := make([]int, 0, len(scores))
for _, score := range scores { if score >= 0 && score <= 100 { result = append(result, score) } }
return result}
func average(values []int) (float64, bool) { if len(values) == 0 { return 0, false }
total := 0 for _, value := range values { total += value }
return float64(total) / float64(len(values)), true}
func main() { scores := []int{95, 82, -5, 74, 103, 61} valid := validScores(scores) sorted := slices.Clone(valid) slices.Sort(sorted)
fmt.Println("Исходные:", scores) fmt.Println("Корректные:", valid) fmt.Println("Отсортированные:", sorted)
value, ok := average(valid) if !ok { fmt.Println("Нет корректных оценок") return }
fmt.Printf("Среднее: %.2f\n", value)}Исходный slice не изменяется при сортировке, потому что сортируется его клон.
28. Типичные ошибки
Выход за границы
values := []int{10, 20, 30}// fmt.Println(values[3]) // panicПоследний допустимый индекс — len(values)-1.
Потеря результата append
Всегда сохраняйте возвращённый slice.
make с неправильной длиной
Если планируется добавление, используйте make([]T, 0, capacity), а не длину,
равную будущему числу элементов.
Ожидание независимой копии
Присваивание и обычный срез сохраняют общее хранилище. Для независимых данных
используйте copy или slices.Clone.
Изменение копии из range
Изменяйте values[index], а не локальную переменную value.
Сортировка исходных данных
slices.Sort работает на месте. Клонируйте данные, если исходный порядок нужно
сохранить.
29. Вопросы для самопроверки
- Почему
[3]intи[4]intявляются разными типами? - Что копируется при передаче массива в функцию?
- Из каких частей концептуально состоит slice?
- Чем
lenотличается отcap? - Когда
appendвыделяет новый массив? - Почему slices после присваивания могут влиять друг на друга?
- Как получить независимую копию?
- Чем
nilslice отличается от пустого? - Почему slices нельзя сравнивать через
==? - Как функция должна добавить элемент в slice вызывающей стороны?
- Почему маленький slice может удерживать большой объём памяти?
- Когда фильтрация на месте опасна?
Итоги
Массив хранит фиксированное число элементов, а slice представляет диапазон
базового массива и содержит длину и вместимость. append может сохранить старое
хранилище или создать новое, поэтому его результат всегда нужно использовать.
Главная практическая сложность slices — совместное базовое хранилище: независимая
копия требует явного клонирования.