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

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 17 из 25 ~20 мин +50 XP

Срезы: создание и индексация

Срезы в go — главная коллекция языка: []int, []string и их родня встречаются в прикладном коде на каждом шагу, отодвинув массивы из урока 16 в ниши вроде хешей и буферов. Здесь разберём, как slice устроен внутри — окно в массив с заголовком из трёх слов, — литералы и make, пару len и cap, nil-срезы, обращение по индексу и полуинтервал s[i:j], после которого два среза вдруг делят одну память. Всё с примерами, которые можно запустить.

Содержание урока · 14 разделов

Срезы в go: зачем они, если есть массивы#

Корзина интернет-магазина: сначала в ней ноль товаров, потом три, к вечеру — тридцать. Тип [3]int из урока про массивы для такой задачи беспомощен: длина вшита в тип, новых ячеек не появится, а передача корзины в функцию скопирует все элементы целиком — массив ведь значение. Для всего, что растёт, сжимается и режется, в языке есть срез — slice; slice golang-коде — основная коллекция: []int, []string, []float64, []byte.

Срезы в go решают обе проблемы массива. Длина не входит в тип: []int из трёх элементов и []int из тридцати — один и тот же тип, они свободно присваиваются друг другу и передаются в одни и те же функции. А при присваивании копируется не содержимое, а крошечный заголовок — три машинных слова, какие бы данные за ним ни стояли. Поэтому сигнатуры прикладного кода набиты срезами: параметр []Order, возврат []string, поле []byte.

За гибкость платят новыми правилами. У среза две длины вместо одной — len и cap. Нулевое значение особое — nil. А данные при копировании не дублируются, а делятся: поменяли в одной переменной — поменялось в другой. Все эти правила мы разберём по очереди, начав с самого простого — создать срез и заглянуть в него.

Песочница: первый срез

Запустите: срез печатается в квадратных скобках одной строкой, len() вернул пять. Индексация работает как у массива — temps[2] это третий элемент, счёт с нуля, а запись temps[4] = 25 поменяла пятницу. Теперь три опыта. Первый: попробуйте прочитать temps[5] — программа оборвётся ошибкой выхода за границу: резервный движок скажет «индекс за границами: 5 (длина 5)», настоящий Go — панику index out of range [5] with length 5; суть одна, индексы кончаются на четырёх. Второй: допишите ещё один элемент в литерал и запустите — тип []int остался тем же, длина не входит в тип среза, никакой правки объявления не нужно. Третий: напечатайте cap(temps) рядом с len(temps) — у свежего литерала числа совпадают, и почему это важно, станет ясно в середине урока.

Вывод

Запустите код, чтобы увидеть вывод.

Срез — окно в массив: три слова заголовка#

Вот картинка, которая объясняет все странности срезов разом. Данные среза лежат в обычном массиве — непрерывном ряде ячеек памяти, где элементы идут подряд. Сам срез — это не ряд, а окно, наведённое на часть ряда: маленький заголовок из трёх машинных слов. Первое — указатель, адрес ячейки, с которой окно начинается. Второе — len, сколько элементов видно через окно прямо сейчас. Третье — cap, ёмкость: сколько элементов можно было бы видеть, сдвинув правый край окна до конца массива.

Когда вы пишете t := s[1:3], ничего не копируется — заводится второе окно на ту же память, с указателем на вторую ячейку и len 2. Оба окна смотрят на одни ячейки, поэтому правка через одно видна через другое — это главный сюрприз урока, дойдём до него шаг за шагом. А пока запомните связку: массив — это сами ячейки, срез — окно на них. Массив копируется целиком, окно — тремя словами. Отсюда и дешевизна передачи среза в функцию: три слова в любом случае, хоть за окном миллион элементов.

Заголовок — внутреннее устройство, руками его не достать: указатель и cap не печатаются при выводе среза, видно только len элементов. Но len() и cap() дают два из трёх слов напрямую, и через них всё окно раскладывается на части.

Литерал среза: []T{...}#

Создаётся срез чаще всего литералом — перечислением значений в фигурных скобках после типа. Тип пишется с пустой парой квадратных скобок: []int{18, 21, 19} — срез из трёх чисел, []string{"go", "rust"} — из двух строк, [][]int{{1, 2}, {3, 4}} — срез срезов, о нём ближе к концу. Форма обманчиво похожа на литерал массива [3]int{...} — отличается лишь отсутствием числа в скобках, но разница принципиальная: здесь длина не вшита в тип, а считается по числу элементов в скобках.

Пустой литерал []int{} законен: это срез нулевой длины, готовый к заполнению — классическая заготовка под будущий append. Форма var s = []int{...} с полным типом тоже работает, как и у массивов; короткое объявление s := []int{...} практичнее, и дальше используем его. А вот [...] для срезов нет: две точки — это фишка массивов, где компилятору предлагают посчитать длину; срезу считать нечего, длина и так не входит в тип. Внутри литерала типы должны совпадать: []int{1, "два"} не скомпилируется ни в одном движке — никаких сюрпризов.

Литерал хорош, когда элементы известны на месте. Когда количество приходит в рантайме — из переменной, из ответа сервера, из аргументов, — нужен make, и у него будет отдельный раздел ниже.

Проверьте себя: []int против [3]int

В программе рядом написаны a := []int{1, 2, 3} и b := [3]int{1, 2, 3}. Какое утверждение верно?

[]T против [N]T: длина не входит в тип#

Разница типов — не синтаксический украшение, а рабочее правило, которое меняет поведение программы. Первое следствие: срезы разных длин совместимы. s := []int{1} и большой := []int{1, 2, 3, 4, 5} имеют один тип []int, поэтому s = большой — законное присваивание, и функция с параметром []int примет срез любого размера. Для массивов то же не проходит: [1]int и [5]int — два разных типа, и попытка присвоить один другому не компилируется.

Второе следствие: массив нельзя присвоить срезу и наоборот. var s []int = arr, где arr — массив, — ошибка компиляции даже при совпадающих длинах: это разные типы. Мост между ними — операция среза массива: s := arr[:] делает окно на весь массив, и вот уже тип []int; об операции среза ниже, она же работает на массиве. Третье следствие видят все, кто печатает типы: fmt.Printf с глаголом %T для среза напечатает []int — и в песочнице, и на настоящем компиляторе; для массива основной движок покажет [3]int, а запасной резервный интерпретатор отметится своей известной странностью из урока 16.

И ещё одна грань того же правила: тип []T описывает только ЧТО лежит внутри, но не СКОЛЬКО. Поэтому длина всегда ходит рядом со срезом — её возвращает len(), её передают в функции вторым аргументом, её проверяют в циклах. В сигнатуре func sum(nums []int) int нет размера — он приедет в значении.

Тип на экране: %T у среза

GO
package main

import "fmt"

func main() {
        nums := []int{1, 2, 3}
        words := []string{"go", "rust"}

        fmt.Printf("%T\n", nums)
        fmt.Printf("%T\n", words)
}

Блок ранимый — оба движка дают одинаковый вывод: «[]int» и «[]string». Глагол %T печатает тип значения, и тип среза записывается именно так, как в объявлении: пустые квадратные скобки и тип элемента. Для сравнения: у массива [3]int основной движок напечатает [3]int, а резервный упрощённый — свою версию с interface {}, поэтому массив в ранимых примерах урока не фигурирует. Запустите блок и убедитесь, что тип из объявления и напечатанный тип совпадают буква в букву.

len и cap: сколько лежит и сколько влезает#

Вот два вопроса, которые задаёт любой срез: сколько элементов видно сейчас и сколько влезло бы, если раздвинуть окно до конца массива. Первый отвечает len(), второй — cap(). Диаграмма в словах: за срезом лежит гребёнка из семи ячеек; окно среза наставлено на ячейки со второй по четвёртую — через него видно три ячейки, len = 3; от начала окна до правого края гребёнки — шесть ячеек, cap = 6. len — про настоящее, cap — про запас.

У свежесозданных срезов оба числа совпадают. Литерал []int{18, 21, 19} — окно на весь массив из трёх ячеек, len 3 и cap 3; make([]int, 4) — четыре нуля, len 4 и cap 4. Это удобно запомнить и удобно использовать: в этом уроке пара len cap в go-примерах всегда равна, и только операция среза s[i:j] разводит числа — в настоящем Go у результата len станет j-i, а cap останется равным расстоянию от начала окна до конца массива, то есть cap(s) минус i. Резервный движок cap после срезания считает иначе — эта тонкость собрана в честном списке в конце урока, в ранимых примерах cap после срезания не используется.

Пока len и cap всегда равны, вопрос «зачем два числа» звучит справедливо. Расходятся они только при росте среза: операция append дописывает элементы, len ползёт вверх, cap двигается скачками — запас, который язык копит под будущие дописывания. Рост среза — тема следующего урока, и там cap из пассивного наблюдения превратится в рабочий инструмент.

Что выведет код?

Прочитайте код и выберите, что он выведет:

package main

import "fmt"

func main() {
        s := []int{1, 2, 3, 4, 5}
        fmt.Println(len(s), cap(s))
}

nil-срез и пустой срез: разница без разницы#

У каждой переменной в Go есть нулевое значение, и у среза оно особое: nil. Объявление var s []int без литерала даёт nil-срез — заголовок, который никуда не смотрит: окна нет, есть только три нулевых слова. Второй способ получить «пустоту» — пустой литерал s := []int{}: заголовок есть, окно наставлено на массив нулевой длины, элементов всё равно нет. Разница философская, но у неё есть измеримые проявления, и все они проверены пробником на обоих движках.

len() у обоих возвращает 0 — здесь nil-срез и пустой срез неотличимы. Сравнение с nil разводит их: у nil-среза s == nil даёт true, у пустого литерала — false. Практическое правило идиоматичного Go: на вопрос «пуст ли срез» отвечайте len(s) == 0 — это выражение истинно для обоих случаев и не заставляет читать код дальше. А вот печатать nil-срез в ранимых примерах мы не будем: настоящий Go выведет пустые скобки [], резервный упрощённый — слово <nil>; вывод разъезжается, поэтому в runnable-блоке урока nil-срез проверяется len и сравнением, а его печать остаётся текстовой темой.

Индексация nil-среза ошибки не прощает: s[0] при len 0 — выход за границу в обоих движках. Перебирать nil-срез безопасно — range сделает ноль оборотов. append к nil-срезу законен и создаёт данные — но это уже следующий урок.

make: срез нужной длины#

Вторая половина способов создать срез — функция make. make([]int, 4) выделяет массив на четыре ячейки, заполняет нулями и возвращает окно на все четыре: len 4, cap 4, печать даёт [0 0 0 0]. Для строк то же самое: make([]string, 3) — три пустые строки. make берёт тип и длину, остальное делает сам — это способ сказать языку «дай срез вот такого размера», не выписывая элементы руками.

Главное преимущество перед литералом: размер может быть переменной. n := 4; buf := make([]int, n) — законно и типично; литерал так не умеет, в нём элементы выписываются по одному, и переменной длины не бывает. Поэтому весь код вида «размер пришёл из данных» пишется через make: буфер на n сообщений, сетка на rows строк, палитра на colors цветов.

Заполняется make-срез по индексу — сам он нулей не меняет: buf[i] = ... в цикле, чаще всего range с одной переменной-индексом, как отрабатывали в уроке 15. У make есть и третья форма — make([]int, 3, 10) с ёмкостью; она связана с ростом срезов, и про неё есть отдельный честный callout сразу после примера: с этой формой движки песочницы расходятся, поэтому в ранимых примерах урока make вызывается только с длиной.

Песочница: make и заполнение

Запустите: make выдал четыре нуля, len и cap равны четырём — размер пришёл из переменной n, для литерала такая запись невозможна. Цикл range с одной переменной-индексом заполнил буфер десятками. Опыты: поднимите n до 7 и запустите снова — править больше ничего не пришлось, make сам подстроился под новый размер, len и cap теперь семь. Затем при n = 4 попробуйте buf[7] = 1 — оба движка оборвут программу: make выделил ровно столько ячеек, сколько попросили, и запись за границей так же незаконна, как чтение. И напоследок замените (i + 1) * 10 на i * i — буфер станет квадратами индексов.

Вывод

Запустите код, чтобы увидеть вывод.

Обращение по индексу: от нуля до len-1#

Индексация среза дословно повторяет массив из урока 16, и это хорошо: переучиваться не придётся. Индексы идут от 0 до len-1; s[0] — первый элемент, s[len(s)-1] — последний. Чтение и запись законны в этих пределах: s[2] = 30, s[0] *= 2, s[i]++ — всё работает. За пределами — ошибка. Чтение s[5] на срезе длины 5 роняет программу: резервный движок отвечает «индекс за границами: 5 (длина 5)», настоящий Go паникует index out of range [5] with length 5. Отрицательный индекс — тоже ошибка в обоих движках: за границей считается всё, что не в 0..len-1.

Классическая ловушка — s[len(s)]: на чтении последнего элемента рука сама тянется к len(s), но правильный индекс на единицу меньше. Вторая ловушка родом из других языков: запись за границей в Go НЕ растягивает срез. В Python список доживает до присваивания s[5] = 1, в Go это ошибка — срез не растёт записью, ростом занимается только append из следующего урока. Привыкайте: размер меняется явно, через make или append, а индексация работает внутри уже существующих ячеек.

Индексом может быть любое выражение с числом: переменная цикла, арифметика вроде s[i+1], результат функции. Главное — чтобы значение попало в 0..len-1, иначе та же ошибка выхода за границу.

Проверьте себя: копия или общие данные

s := []int{5, 0, 9}, затем t := s и t[0] = 9. Что напечатает fmt.Println(s)?

Операция среза: полуинтервал s[i:j]#

Самая мощная операция срезов прячется в знакомой синтаксисе индексации: s[i:j] — вырезать окно из окна. Запись означает «элементы с индекса i по индекс j-1 включительно»: правая граница НЕ входит в результат. Такая граница называется полуинтервалом, и её логика практична: длина результата считается в уме как j-i, а соседние окна стыкуются без наложений — s[:2] и s[2:] делят исходный срез ровно пополам, ничто не теряется и не дублируется. У []int{1, 2, 3, 4, 5} вырезка s[1:3] — это [2 3]: индексы 1 и 2, тройка с индексом 3 осталась за бортом.

Границы можно опускать с обеих сторон: s[1:] — от первого индекса до конца, s[:2] — от начала до второго индекса, s[:] — весь срез, копия заголовка. Пропуски — не элегантность, а способ не писать len(s) в каждой вырезке: s[1:] короче и честнее, чем s[1:len(s)]. Правая граница может равняться len(s), но не превышать его; левая не должна заходить за правую. Нарушения в настоящем Go — паника slice bounds out of range; резервный движок молча зажимает границы и ошибки не даёт — не полагайтесь на эту разницу, соблюдайте 0 <= i <= j <= len(s) всегда.

Операция среза работает и на массивах — и это мост из урока 16: arr := [5]int{1, 2, 3, 4, 5}; s := arr[1:3] — срез массива даёт именно срез, тип []int, окно на память массива. Массив остаётся значением, но теперь на него смотрит окно, и правки через окно достаются до массива. Результат срезания — всегда срез, даже если нарезать массив или строку.

Что выведет код: вырезка

Прочитайте код и выберите, что он выведет:

package main

import "fmt"

func main() {
        s := []int{1, 2, 3, 4, 5}
        fmt.Println(s[1:3])
}

Срез делит память с оригиналом

GO
package main

import "fmt"

func main() {
        s := []int{5, 0, 9}
        t := s[1:3]
        t[0] = 9

        fmt.Println(s)
        fmt.Println(t)
}

Настоящий Go — и основной движок песочницы — напечатает «[5 9 9]» и «[9 9]»: окно t наставлено на те же ячейки, что и s, запись t[0] = 9 попала прямо в s[1]. Кнопки у блока нет: запасной TS-интерпретатор при срезании копирует данные и покажет s нетронутым — «[5 0 9]», а блок обязан работать одинаково в любом режиме. Итог для памяти: присваивание среза делит данные в обоих движках (проверяли quiz-ом), вырезка s[i:j] делит данные в настоящем Go — проверяйте этот пример на локальном компиляторе или основном движке песочницы.

Песочница: срезы срезов

Запустите: срез может быть элементом другого среза — тип [][]int, а полки разной длины, язык это допускает («рваный» список, в отличие от массивов, где строки обязаны совпадать). len(shelf) считает полки, вложенный range отдаёт индекс и строку целиком, len(row) — длину каждой полки. Запись shelf[1][2] = 9 попала в напечатанный срез: внутренние срезы делят память так же, как обычные. Опыты: допишите четвёртую полку в литерал — полок станет четыре; напечатайте shelf[0][1] и замените его на 100; попробуйте прочитать shelf[0][5] — на полке только две ячейки, оба движка оборвут программу ошибкой выхода за границу.

Вывод

Запустите код, чтобы увидеть вывод.

Перебор среза: for и range#

Перебирается срез теми же двумя циклами, что и массив. Классический for со счётчиком: аккумулятор объявляется до цикла, индекс бегёт от 0 до len(s) не включительно, внутри читаем s[i]. Range — короче и типичнее: подробно он разобран в уроке 15, здесь только прикладная выжимка. Две переменные — for i, v := range s — дают индекс и копию значения; одна — for i := range s — только индекс, форма для записи по месту; подчёркивание for _, v := range s — когда нужен лишь смысл элемента. Внутри тела работают break и continue с правилами из урока 14 — range останавливается на break так же, как классический цикл.

Единственное, что требует постоянного внимания, — v из range копия. Присвоите v = v 10 — изменится копия, срез не дрогнет; чтобы поменять элементы, пишите по индексу: s[i] = 2. Двумерный срез перебирается вложенным range: внешний проходит полки, внутренний — значения внутри полки; строка из range — тоже копия заголовка, но данные за ней общие, поэтому row[0] = 9 из range всё равно изменит срез — тот самый общий заголовок.

Практика: перебор среза не может зациклиться — range всегда доходит до конца и останавливается; границей служит len, зафиксированный в заголовке цикла. Вечный цикл на range не написать — для этого есть вечный for из урока 14.

Вставьте пропуски: срез из нулей

Допишите создание и заполнение: срез из трёх нулей через make и печать среза вместе с длиной. Правильная программа печатает «[0 0 9] 3».

s := ([]int, 3) s[2] = 9 fmt.Println(s, (s))

Соберите программу: перебор среза

Соберите программу: литерал среза строк, range с индексом и значением и финальная строка после цикла. Собранный вывод — «0 раз», «1 два», «2 три», «готово».

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Шпаргалка: операции со срезами#

ЗаписьЧто делаетВ песочнице
s := []int{5, 0, 9}литерал: срез из трёх чиселработает, вывод и len/cap совпадают в обоих движках
s := []int{}пустой срез, len 0работает
var s []intnil-срез, нулевое значениеlen 0 и s == nil работают; печать nil-среза в резерве даёт <nil> вместо []
s := make([]int, 4)четыре нуля, len 4 cap 4работает
s := make([]int, 3, 10)len 3, ёмкость 10основной движок cap 10, резервный cap 3 — ранимым не делаем
s[i] / s[i] = vчтение и запись по индексу, 0..len-1работает
s[5] при len 5ошибка выхода за границуоба движка роняют программу, текст ошибки разный
t := s[1:3]полуинтервал, len 2печать и len работают; cap после срезания в резерве занижен
t := sкопия заголовка, данные общиеработает: правка через t видна в s
s[0:10] при len 3ошибка в настоящем Go (slice bounds)резервный молча зажимает границу — проверяйте на настоящем Go
s[1:3:5]трёхиндексный срез: len и capрезервный молча игнорирует третий индекс — ранимым не делаем

Карточки: срезы

Термин

Чем срез []int отличается от массива [3]int?

нажмите, чтобы перевернуть
Определение

У среза длина не входит в тип: []int любой длины — один тип, размер меняется. Массив держит длину в типе, [3]int и [4]int — разные типы. При присваивании срез копирует заголовок из трёх слов, массив — все элементы.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Срез — главная коллекция Go: длина не входит в тип []T, размер не фиксируется, а копируется заголовок из трёх слов, а не данные.
  • Срез — окно в массив: указатель на начало, len (сколько видно) и cap (сколько влезает до конца массива); len и cap у свежего среза равны.
  • Создание: литерал []int{5, 0, 9}, пустой []int{}, make([]int, 4) для переменной длины; nil-срез var s []int и пустой срез имеют len 0, различает их только сравнение с nil.
  • Индексация — от 0 до len-1; выход за границу роняет программу в обоих движках, запись срез не растягивает — ростом занимается append.
  • s[i:j] — полуинтервал: правая граница не включается, len результата j-i; границы опускаются: s[1:], s[:2], s[:]; срез массива arr[1:3] даёт срез []int.
  • Срез делит память: t := s; t[0] = 9 меняет s, и окно после вырезки смотрит на те же ячейки — в настоящем Go правка через t достаётся оригиналу.
  • Перебор — классическим for с len() или range из урока 15; v в range — копия, менять срез можно только по индексу.

Домашнее задание

Соберите недельный отчёт на срезах: temps := []int{18, 21, 19, 24, 26, 22, 20}. Классическим for с len() посчитайте сумму — должно выйти 150. Range-ом с подчёркиванием найдите максимум — 26. Вырежьте выходные: t := temps[5:] и напечатайте t — получится [22 20]. Затем сделайте squares := make([]int, 5), заполните его квадратами чисел 10, 20, 30, 40, 50 через range с индексом и напечатайте вместе с длиной: [100 400 900 1600 2500] 5. Все четыре задачи решаются приёмами этого урока — append не понадобится, он в следующем уроке.

Личные заметкиПусто
Хранятся только в вашем браузере (Go Base · Срезы: создание и индексация)

Вопросы и ответы по уроку

Чем срез отличается от массива и почему в коде почти всегда срезы?

Длина среза не входит в тип, размер меняется, а присваивание копирует заголовок из трёх слов, не данные. Массив держит длину в типе и копируется целиком. Поэтому сигнатуры функций, возвраты и поля структур набиты срезами; массивы остаются в нишах — хеши [32]byte, буферы, сетки.

Зачем срезу cap, если len и cap у литерала равны?

Равенство держится у свежесозданных срезов. После вырезки s[i:j] в настоящем Go cap становится расстоянием от окна до конца массива за ним, а при росте через append запас ёмкости экономит пересоздания массива. В резервном движке песочницы cap после срезания считается иначе, поэтому в этом уроке cap берётся только у свежих срезов.

Что означает ошибка «индекс за границами» и как её избежать?

Индексы среза идут от 0 до len-1; обращение к s[len(s)] или отрицательное значение роняет программу. Перед обращением по переменному индексу проверяйте len; последний элемент — s[len(s)-1]. Срезы срезания подчиняются правилу 0 <= i <= j <= len(s), иначе настоящий Go паникует slice bounds out of range.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Go Pro25 уроков~14 чпрактика25 уроков уже доступны

Продвинутые возможности Go: от замыканий до рефлексии

Продвинутый курс Go: замыкания, defer, горутины, каналы, контекст, интерфейсы, дженерики, рефлексия, тесты и JSON. 25 уроков с практикой в браузере.

  • Писать и читать конкурентный код: go-функции, каналы, select
  • Управлять жизненным циклом операций через context
  • Проектировать API на интерфейсах
  • Использовать дженерики без злоупотребления