Срезы
В прошлом уроке ряд шкафчиков был привинчен к полу: длину массива нужно знать заранее. Но число покупок в корзине или строк в файле программа узнаёт только во время работы.
Поэтому в коде на 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.
Давайте разберём пример по строкам:
s := []int{10, 20, 30}создаёт срез из трёх элементов.s[1]читает второй элемент,20, аlen(s)возвращает3, поэтому строка печатает20 3.s[0] = 15кладёт новое значение в первый шкафчик.- Второй
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 принимает только массивы из трёх элементов.
На собеседовании часто просят назвать главное отличие массива от среза:
- длина массива входит в его тип и не меняется;
- длина среза в тип не входит, а сами элементы лежат в массиве, на который срез смотрит.
Сравнение срезов
Этот вопрос тоже часто задают на собеседовании: можно ли сравнить два среза оператором ==? Массивы одного типа сравнивать можно: == проверяет их элементы по порядку.
a := [3]int{1, 2, 3}
b := [3]int{1, 2, 3}
fmt.Println(a == b) // trueДля двух срезов та же запись s == t не компилируется:
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 — копия элемента с этим индексом:
s := []int{4, 8, 15}
for i, v := range s {
fmt.Print(i, ":", v, " ") // 0:4 1:8 2:15
}Цикл делает три итерации:
iравна0, аv—4.iравна1, аv—8.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) включительно:
s := []int{10, 20, 30}
for i := 0; i <= len(s); i++ { // ✗
fmt.Println(s[i])
}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 и подпишись на канал — откроются все уроки.