Перейти к содержимому

Лекция 5. Массивы и slices

Цели лекции

После лекции студент должен:

  • различать массив и slice;
  • объявлять и инициализировать массивы;
  • создавать slices через литерал, срез массива и make;
  • понимать len и cap;
  • безопасно применять append и сохранять его результат;
  • понимать совместное базовое хранилище slices;
  • копировать данные через copy и slices.Clone;
  • отличать nil slice от пустого;
  • передавать slices в функции;
  • избегать ошибок границ и случайного изменения общих данных.

1. Зачем нужны последовательности

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

В Go есть два тесно связанных типа:

  • array — массив фиксированной длины;
  • slice — представление последовательности переменной длины поверх массива.

В прикладном коде slices встречаются значительно чаще, но понимание массива необходимо для понимания их устройства.

2. Массив

Тип массива включает длину:

var scores [5]int

scores содержит ровно пять элементов типа int. После объявления элементы получают zero value:

fmt.Println(scores) // [0 0 0 0 0]

Обращение по индексу:

scores[0] = 90
scores[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 []string

Slice можно создать литералом:

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)) // 2
fmt.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)) // 5
fmt.Println(cap(values)) // 5

Можно отдельно задать вместимость:

values := make([]int, 0, 10)
fmt.Println(len(values)) // 0
fmt.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 := numbers
copyOfHeader[0] = 777
fmt.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) // 3
fmt.Println(destination) // [10 20 30]

copy возвращает количество скопированных элементов — минимум длин источника и назначения.

source := []int{1, 2, 3, 4}
destination := make([]int, 2)
fmt.Println(copy(destination, source)) // 2
fmt.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 []int
second := []int{}
third := make([]int, 0)

У всех длина равна нулю:

fmt.Println(len(first), len(second), len(third)) // 0 0 0

Только первый равен nil:

fmt.Println(first == nil) // true
fmt.Println(second == nil) // false

append работает с 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)) // true

19. Перебор 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 := 2
numbers = 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. Вопросы для самопроверки

  1. Почему [3]int и [4]int являются разными типами?
  2. Что копируется при передаче массива в функцию?
  3. Из каких частей концептуально состоит slice?
  4. Чем len отличается от cap?
  5. Когда append выделяет новый массив?
  6. Почему slices после присваивания могут влиять друг на друга?
  7. Как получить независимую копию?
  8. Чем nil slice отличается от пустого?
  9. Почему slices нельзя сравнивать через ==?
  10. Как функция должна добавить элемент в slice вызывающей стороны?
  11. Почему маленький slice может удерживать большой объём памяти?
  12. Когда фильтрация на месте опасна?

Итоги

Массив хранит фиксированное число элементов, а slice представляет диапазон базового массива и содержит длину и вместимость. append может сохранить старое хранилище или создать новое, поэтому его результат всегда нужно использовать. Главная практическая сложность slices — совместное базовое хранилище: независимая копия требует явного клонирования.