Содержание урока · 14 разделов
- Срезы в go: зачем они, если есть массивы
- Срез — окно в массив: три слова заголовка
- Литерал среза: []T{...}
- []T против [N]T: длина не входит в тип
- len и cap: сколько лежит и сколько влезает
- nil-срез и пустой срез: разница без разницы
- make: срез нужной длины
- Обращение по индексу: от нуля до len-1
- Операция среза: полуинтервал s[i:j]
- Перебор среза: for и range
- Шпаргалка: операции со срезами
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Срезы в 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 у среза
GOpackage 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 — буфер станет квадратами индексов.
Запустите код, чтобы увидеть вывод.
make([]int, 3, 10) в настоящем Go создаёт срез длины 3 с ёмкостью 10: за окном лежит массив на десять ячеек, но видно пока три — запас под будущий append. Основной движок песочницы покажет len 3 cap 10. Запасной TS-интерпретатор третий аргумент понимает упрощённо и вернёт cap 3. Поэтому в ранимых примерах урока make вызывается только с длиной — на этой форме оба движка совпадают, а трёхаргументная форма останется темой урока про append, где она на своём месте.
Обращение по индексу: от нуля до 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])
}Срез делит память с оригиналом
GOpackage 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».
Соберите программу: перебор среза
Соберите программу: литерал среза строк, range с индексом и значением и финальная строка после цикла. Собранный вывод — «0 раз», «1 два», «2 три», «готово».
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Шпаргалка: операции со срезами#
| Запись | Что делает | В песочнице |
|---|---|---|
| s := []int{5, 0, 9} | литерал: срез из трёх чисел | работает, вывод и len/cap совпадают в обоих движках |
| s := []int{} | пустой срез, len 0 | работает |
| var s []int | nil-срез, нулевое значение | 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 | резервный молча игнорирует третий индекс — ранимым не делаем |
Каждый пример урока прогнан пробником на обоих движках песочницы. Совпадают: литералы, len, cap у свежего среза и make с длиной, индексация, ошибка выхода за индекс (факт, не текст), печать и длина результата s[i:j], открытые границы, передача среза в функцию, срезы срезов, поведение nil-среза по len и ==. Расходятся, все случаи проверены наблюдениями: (1) печать nil-среза — [] против <nil>; (2) make с тремя аргументами — cap 10 против 3; (3) cap после срезания — настоящий Go считает от начала окна до конца массива (cap(s) - i), резервный — просто длину результата; (4) правка через окно s[1:3] меняет оригинал в основном движке, резервный копирует; (5) срез за границей s[0:10] и перепутанный s[2:1] — настоящий Go паникует, резервный зажимает границы молча; (6) трёхиндексный s[1:3:5] резервный сводит к s[1:3]; (7) %q и %T на вложенных срезах резервный печатает упрощённо. Все эти формы сознательно вынесены из ранимых блоков урока.
Карточки: срезы
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +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 не понадобится, он в следующем уроке.
Личные заметкиПусто
Вопросы и ответы по уроку
Чем срез отличается от массива и почему в коде почти всегда срезы?
Длина среза не входит в тип, размер меняется, а присваивание копирует заголовок из трёх слов, не данные. Массив держит длину в типе и копируется целиком. Поэтому сигнатуры функций, возвраты и поля структур набиты срезами; массивы остаются в нишах — хеши [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: от замыканий до рефлексии
Продвинутый курс Go: замыкания, defer, горутины, каналы, контекст, интерфейсы, дженерики, рефлексия, тесты и JSON. 25 уроков с практикой в браузере.
- Писать и читать конкурентный код: go-функции, каналы, select
- Управлять жизненным циклом операций через context
- Проектировать API на интерфейсах
- Использовать дженерики без злоупотребления