Урок курса

Массивы и срезы: создание, индексирование, len, cap, append и range

Golang с нуля

До этого урока вы работали с отдельными переменными и функциями, которые их принимают. Как только данных становится больше одного — нужна коллекция. Go предлагает два инструмента: массив с фиксированной длиной и срез, который умеет расти. Они похожи внешне, но устроены по-разному, и путать их дорого.

Массив [N]T: тип, литерал, индексирование, len и cap

В Go тип массива записывается как [N]T, где N — числовая константа, а T — тип элементов. Важная деталь: N — это часть самого типа. [3]int и [5]int — два разных типа, точно так же как int и string. Присвоить одно другому или передать в функцию, ожидающую [3]int, массив [5]int — ошибка компиляции.

Создаётся массив литералом:

a := [3]int{10, 20, 30}

Здесь a имеет тип [3]int. Элементы нумеруются с нуля, последний доступный индекс — N−1, то есть для [3]int это индексы 0, 1, 2.

Чтение и запись — стандартные:

fmt.Println(a[0]) // 10
a[2] = 99
fmt.Println(a[2]) // 99

Что произойдёт, если выйти за границу? Здесь важно различать два случая.

Константный индекс — компилятор знает значение во время сборки и отклоняет программу сразу:

fmt.Println(a[3]) // ошибка компиляции: invalid argument: index 3 out of bounds [0:3]

Эта программа не соберётся вообще.

Переменный индекс — компилятор не может проверить его заранее, поэтому проверка переносится в момент выполнения. Если значение окажется вне диапазона, возникает panic:

i := 3
fmt.Println(a[i]) // runtime panic: index out of range [3] with length 3

Программа скомпилируется, но упадёт при запуске. Это не исключение, которое можно перехватить в обычном коде, — выполнение прекращается.

len(a) возвращает 3. cap(a) тоже возвращает 3 — у массива нет отдельной ёмкости, она совпадает с длиной и зафиксирована навсегда. Это принципиальное отличие от среза.

По синтаксису типа легко отличить массив от среза: [3]int — массив, []int — срез. Квадратные скобки без числа означают срез.

Срез []T: литерал, make([]T, len, cap), len, cap и nil-срез

Срез []T — динамическая последовательность. В отличие от массива, у него нет числа в скобках, и его длина не является частью типа: все []int — один и тот же тип, сколько бы элементов в них ни было.

Два способа создать срез — литерал и make.

Литерал:

s := []int{1, 2, 3}
fmt.Println(len(s), cap(s)) // 3 3

Здесь len и cap совпадают, потому что срез создан ровно под три элемента.

make даёт больше контроля:

s := make([]int, 3, 5)
fmt.Println(len(s), cap(s)) // 3 5

Доступны только те индексы, что меньше len: s[0], s[1], s[2] — всё, эти три элементы равны нулю. Попытка обратиться к s[3] или s[4] вызовет панику, потому что они выходят за пределы длины. Правило простое: допустимый диапазон индексов — от 0 до len(s)−1, и никак иначе. append возвращает срез с увеличенной длиной, открывая тем самым новые индексы — но об этом в следующей секции.

Если третий аргумент опустить, cap равен len:

s := make([]int, 4)
fmt.Println(len(s), cap(s)) // 4 4

Инвариант всегда один: len(s) <= cap(s). Это гарантия языка, нарушить её через обычный код невозможно.

Отдельный случай — nil-срез. Это нулевое значение типа []T:

var s []int
fmt.Println(s == nil)       // true
fmt.Println(len(s), cap(s)) // 0 0

nil-срез — не ошибка. Он валиден, его можно передавать в функции и применять к нему append. Просто в нём пока ноль элементов и ноль ёмкости.

append с обязательным присваиванием и for range по срезу

append — единственный стандартный способ добавить элемент в срез. Он принимает срез и один или несколько новых значений, а возвращает обновлённый срез:

s := []int{1, 2, 3}
s = append(s, 4)
fmt.Println(s) // [1 2 3 4]

Обычная форма в этом уроке — s = append(s, v). Главное правило: используйте возвращённый срез. Его длина увеличена на число добавленных элементов, а базовый массив мог сохраниться или смениться — это зависит от текущей ёмкости и остаётся деталью реализации.

Результат append можно также присвоить другой переменной, вернуть из функции или сразу передать следующему вызову. Что недопустимо — полностью игнорировать возвращённое значение:

append(s, 4) // ошибка компиляции: append(s, 4) evaluated but not used

Программа не соберётся. Go требует, чтобы возвращённый срез был использован.

append отлично работает с nil-срезом — вы видели его в предыдущей секции:

var s []int
s = append(s, 10, 20)
fmt.Println(s) // [10 20]

Для перебора среза используйте цикл for range:

s := []string{"go", "is", "fun"}
for i, v := range s {
    fmt.Println(i, v)
}
// 0 go
// 1 is
// 2 fun

i — индекс, v — копия элемента. Это важно. Если написать:

for _, v := range s {
    v = "changed" // не меняет s
}

— срез останется нетронутым. v — локальная переменная итерации, изменение которой не отражается на оригинале. Чтобы изменить элемент, нужно присваивание по индексу:

for i := range s {
    s[i] = "changed"
}
fmt.Println(s) // [changed changed changed]

Форма for i := range s даёт только индекс — используйте её, когда значение элемента в теле цикла не нужно. Если нужно только значение без индекса, отбросьте индекс через _:

for _, v := range s {
    fmt.Println(v)
}

Самопроверка: append не гарантирует отдельный массив

Создайте срез и его подсрез, измените элемент подсреза и посмотрите на исходный. Затем добавьте элемент через append. Если capacity хватает, добавление может использовать прежний базовый массив, а при нехватке памяти выделяется новый. Поэтому вывод «после append срезы всегда независимы» неверен.

Для копирования создайте целевой срез подходящей длины и вызовите copy. Проверьте именно length, а не только capacity: copy записывает не больше минимальной длины источника и назначения. Указатель на общий массив объясняет поведение лучше, чем запоминание отдельных примеров. Разберите это до задач, где функция изменяет полученный slice.

Продолжите разбор: Разделяемый базовый массив, copy и подсрезы.

Задача 1

Чем тип [4]int отличается от типа []int в Go?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 2

Срез создан как s := make([]int, 3, 7). Каковы len(s) и cap(s) сразу после создания?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 3

Что выведет следующая программа? Введите ровно те строки, которые появятся в stdout.

package main

import "fmt"

func main() {
    s := []int{10, 20, 30}
    for i, v := range s {
        v = v * 2
        _ = i
    }
    fmt.Println(s)
}
Без регистрации · результат не сохраняется

Задача 4

Фильтр положительных значений

Контроллер получает целые показания от датчика. По правилам этой системы в итоговый отчёт попадают только значения больше нуля; ноль и отрицательные значения нужно отбросить.

Показания уже собраны в последовательность. Нужно отобрать положительные значения, сохранив тот порядок, в котором они пришли.

Что нужно сделать

Допишите keepPositive. Функция должна вернуть новый срез только с положительными элементами входного среза, сохранив их порядок. Ноль положительным не считается. Создайте результат через make, обойдите вход через range и сохраняйте результат append.

Формат ввода

Сначала число n, затем n целых чисел.

Формат вывода

В первой строке количество оставшихся элементов. Во второй строке — сами элементы через пробел. Если элементов нет, вторая строка пустая.

Пример

Ввод:
6
-3 5 0 -1 8 2

Вывод:
3
5 8 2
РешениеGo

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 5

Сводка температур за неделю

Метеорологический журнал давно стал привычным инструментом для тех, кто хочет понять, каким выдался минувший отрезок времени. Семь дней подряд фиксируются показания термометра, и к концу недели набирается полный ряд цифр, каждая из которых отражает один конкретный день — без пропусков и без перестановок.

Когда неделя закрыта, возникает естественное желание подвести итог: не листать записи поочерёдно, а получить единую картину. Для этого нужно выяснить, какой день оказался самым холодным, какой — самым тёплым, и насколько типичной была температура в целом. Именно эти три ориентира дают ощущение недели как единого целого.

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

Что нужно сделать

Реализуйте функцию WeeklyTemperatureSummary(temperatures [7]int) []int. Она должна обойти все семь элементов массива и вернуть срез из трёх чисел в строгом порядке: минимальная температура, максимальная температура, целое среднее. Среднее вычисляется обычным целочисленным делением суммы на 7.

Ввод и вывод

Функция получает один массив ровно из семи целых чисел. Она возвращает срез [минимум, максимум, среднее]. Читать stdin и печатать результат не нужно.

Пример

WeeklyTemperatureSummary([7]int{-3, 1, 4, 2, 0, 5, -2}) возвращает []int{-3, 5, 1}.

РешениеGo

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задачи к уроку

Все задачи курса →

Продолжить с сохранением прогресса

Перейдите к уроку в учебном кабинете.

Перейти к интерактивному уроку