Перейти к содержимому
Теория · Массивы и срезы · урок 3 из 10

Срезы

В прошлом уроке ряд шкафчиков был привинчен к полу: длину массива нужно знать заранее. Но число покупок в корзине или строк в файле программа узнаёт только во время работы.

Поэтому в коде на Go массивы встречаются редко. Чаще используют срез — последовательность элементов одного типа, длина которой не входит в тип:

GO
package main

import "fmt"

func main() {
    s := []int{10, 20, 30}
    fmt.Println(s[1], len(s)) // 20 3
    s[0] = 15
    fmt.Println(s) // [15 20 30]
}

Срез похож на окно, через которое видна часть ряда шкафчиков:

  • сам ряд — это массив, который Go создаёт для литерала []int{10, 20, 30} незаметно для вас;
  • через окно вы называете номера шкафчиков, достаёте и кладёте вещи;
  • длина среза — сколько шкафчиков видно в окне.

На схеме — срез s из примера. Литерал создал ряд ровно из трёх шкафчиков, и окно открывает их все.

СхемаСрез — окно над массивом

len — сколько шкафчиков видно в окне, cap — сколько их от начала окна до конца ряда. Литерал создаёт ряд ровно под свои элементы, поэтому у s оба числа равны 3.

Давайте разберём пример по строкам:

  1. s := []int{10, 20, 30} создаёт срез из трёх элементов.
  2. s[1] читает второй элемент, 20, а len(s) возвращает 3, поэтому строка печатает 20 3.
  3. s[0] = 15 кладёт новое значение в первый шкафчик.
  4. Второй Println печатает [15 20 30].

Запись и индексы

Тип среза пишут без длины в скобках: []int — срез значений int. Остальное работает как у массива:

  • индексы идут от 0 до len(s) - 1;
  • функция len возвращает текущую длину среза;
  • литерал []int{} создаёт срез без элементов: его длина 0, а fmt.Println печатает его как [].

Срез или массив

Срезы []int{1, 2} и []int{1, 2, 3} имеют один и тот же тип []int. Поэтому одна функция с параметром []int принимает срез любой длины. Для массивов так не выйдет: функция с параметром [3]int принимает только массивы из трёх элементов.

На собеседовании часто просят назвать главное отличие массива от среза:

  • длина массива входит в его тип и не меняется;
  • длина среза в тип не входит, а сами элементы лежат в массиве, на который срез смотрит.

Сравнение срезов

Этот вопрос тоже часто задают на собеседовании: можно ли сравнить два среза оператором ==? Массивы одного типа сравнивать можно: == проверяет их элементы по порядку.

GO
a := [3]int{1, 2, 3}
b := [3]int{1, 2, 3}
fmt.Println(a == b) // true

Для двух срезов та же запись s == t не компилируется:

TEXT
invalid operation: s == t (slice can only be compared to nil)

Вспомните, что срез — это окно на ряд шкафчиков. Два окна могут смотреть на один и тот же ряд или на два разных ряда с одинаковыми вещами. По записи s == t не понять, какое из этих равенств вы имели в виду, и Go не выбирает за вас. Содержимое срезов сравнивают одним из двух способов:

  • проходят оба среза циклом и сравнивают элементы по одному;
  • вызывают функцию slices.Equal(s, t) из пакета slices, которая делает то же самое.

Обход среза в цикле

Срез можно обойти четырьмя формами цикла. Разберём каждую.

Индекс и элемент

Цикл for i, v := range s проходит по срезу от первого элемента к последнему. На каждой итерации i — индекс, а v — копия элемента с этим индексом:

GO
s := []int{4, 8, 15}
for i, v := range s {
    fmt.Print(i, ":", v, " ") // 0:4 1:8 2:15
}

Цикл делает три итерации:

  1. i равна 0, а v — 4.
  2. i равна 1, а v — 8.
  3. i равна 2, а v — 15.

Программа печатает все пары в одну строку: 0:4 1:8 2:15.

Здесь аналогия с окном заканчивается: v — не сам шкафчик, а копия того, что в нём лежит. Запись v = 0 в теле меняет только копию, а сам срез остаётся прежним. Чтобы изменить элемент в цикле, пишут s[i] = 0.

Только элемент

Если индекс не нужен, на его место ставят _: for _, v := range s. С обычным именем вместо _ неиспользуемая переменная остановила бы сборку: declared and not used: i.

Только индекс

Если не нужен элемент, его не пишут вовсе: for i := range s. Эта форма удобна, когда элементы меняют через s[i].

Цикл с тремя частями

Цикл с тремя частями тоже обходит срез: for i := 0; i < len(s); i++. Запись через range короче, и индекс в ней не выходит за границы среза. В цикле с тремя частями границу пишете вы сами, и в ней часто ошибаются.

Частая ошибка

Цикл с тремя частями доходит до len(s) включительно:

GO
s := []int{10, 20, 30}
for i := 0; i <= len(s); i++ { // ✗
    fmt.Println(s[i])
}
TEXT
panic: runtime error: index out of range [3] with length 3

Программа печатает три числа, а на четвёртой итерации останавливается. Индекс 3 выходит за срез длины 3, и Go проверяет это во время работы программы. Такая аварийная остановка — уже знакомая вам паника: строки после неё не выполняются. Поставьте < вместо <= или обходите срез через range.

Главное

  • Срез — последовательность элементов одного типа; его длина не входит в тип.
  • Индексы среза идут от 0 до len(s) - 1, а выход за границы вызывает панику.
  • В for i, v := range s переменная v — копия элемента, а менять элемент нужно через s[i].
  • Два среза оператором == не сравнить: содержимое сравнивают циклом или функцией slices.Equal.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом