Массивы и срезы (slices)
Знакомимся с коллекциями данных: учимся хранить списки значений, добавлять элементы и обрабатывать их в цикле
До сих пор мы работали с отдельными переменными: одно число, одна строка, один результат. Но в реальной жизни данные приходят группами. Список оценок студента, корзина товаров в магазине, имена участников чата — всё это коллекции однотипных элементов. Создавать для каждого элемента отдельную переменную (grade1, grade2, grade3...) было бы безумием. Именно для этого существуют массивы и срезы. Сегодня мы начинаем Модуль 3 — Структуры данных, и первая тема — самые важные коллекции в Go.
📦 Массив — коллекция фиксированного размера
Массив (array) — это набор элементов одного типа, размер которого задаётся при создании и больше не меняется. Представьте коробку с ячейками: вы заранее решили, что ячеек будет пять, и больше туда не поместится.
Объявляется массив так:
package main
import "fmt"
func main() {
// Массив из 5 целых чисел
var grades [5]int
// Заполняем элементы по индексу (нумерация с 0!)
grades[0] = 5
grades[1] = 4
grades[2] = 3
grades[3] = 5
grades[4] = 4
fmt.Println(grades) // [5 4 3 5 4]
}
Разберём по частям. Запись [5]int означает: массив из 5 элементов типа int. Число в квадратных скобках — это размер. Тип элементов идёт сразу после. Каждый элемент доступен по индексу: grades[0] — первый элемент, grades[4] — последний.
💡 Подсказка: индексация в Go (и почти во всех языках программирования) начинается с нуля. Первый элемент — это [0], второй — [1], и так далее. Массив из 5 элементов имеет индексы от 0 до 4. Попытка обратиться к grades[5] приведёт к ошибке.
Можно объявить и заполнить массив сразу:
// Короткая форма — объявление + инициализация
grades := [5]int{5, 4, 3, 5, 4}
fmt.Println(grades) // [5 4 3 5 4]
// Можно указать только часть элементов — остальные будут нулями
partial := [5]int{5, 4}
fmt.Println(partial) // [5 4 0 0 0]
// Массив строк
days := [3]string{"Понедельник", "Вторник", "Среда"}
fmt.Println(days) // [Понедельник Вторник Среда]
Есть ещё один удобный синтаксис — с тремя точками. Go сам посчитает количество элементов:
// Компилятор сам определит размер (будет [4]string)
fruits := [...]string{"яблоко", "банан", "вишня", "груша"}
fmt.Println(fruits) // [яблоко банан вишня груша]
fmt.Println(len(fruits)) // 4
Функция len() возвращает длину массива — количество элементов в нём.
Важно: размер массива — это часть его типа. [3]int и [5]int — это разные типы! Вы не можете присвоить массив из 3 элементов переменной, которая ожидает массив из 5. Именно эта негибкость привела к появлению срезов.
🤔 Проблема массивов
Массивы в Go используются редко. Почему? Потому что в реальных программах вы почти никогда не знаете заранее, сколько элементов будет. Представьте: вы загружаете список товаров из базы данных. Сегодня их 50, завтра 200, через месяц 1000. Массив с фиксированным размером тут не подходит.
Кроме того, при передаче массива в функцию Go создаёт его полную копию. Если массив большой — это медленно и тратит память. Для решения всех этих проблем в Go есть срезы (slices) — гибкие, динамические коллекции, с которыми вы будете работать постоянно.
🔪 Срез (slice) — динамическая коллекция
Срез — это как массив, но без фиксированного размера. Он может расти по мере необходимости. В 99% случаев в Go вы будете использовать именно срезы, а не массивы.
Самый простой способ создать срез:
package main
import "fmt"
func main() {
// Объявляем срез (без числа в скобках!)
grades := []int{5, 4, 3, 5, 4}
fmt.Println(grades) // [5 4 3 5 4]
fmt.Println(len(grades)) // 5
// Доступ к элементам — точно как в массиве
fmt.Println(grades[0]) // 5
fmt.Println(grades[2]) // 3
// Изменение элемента
grades[2] = 4
fmt.Println(grades) // [5 4 4 5 4]
}
Обратите внимание: срез объявляется как []int — пустые квадратные скобки, без числа. Именно отсутствие числа отличает срез от массива. [5]int — массив, []int — срез.
Пустой срез можно объявить и потом наполнять:
// Пустой срез
var numbers []int
fmt.Println(numbers) // []
fmt.Println(len(numbers)) // 0
🏗 Создание среза через make
Встроенная функция make позволяет создать срез заданной длины, заполненный нулевыми значениями:
package main
import "fmt"
func main() {
// make(тип, длина)
scores := make([]int, 5)
fmt.Println(scores) // [0 0 0 0 0]
fmt.Println(len(scores)) // 5
// Теперь можно записать значения по индексу
scores[0] = 95
scores[1] = 87
scores[2] = 92
fmt.Println(scores) // [95 87 92 0 0]
}
make принимает тип среза и его начальную длину. Все элементы получают нулевое значение: 0 для чисел, пустую строку для строк, false для булевых.
У make есть и третий аргумент — ёмкость (capacity). Но сначала разберём, что это такое.
📏 Длина (len) и ёмкость (cap)
У каждого среза есть два свойства:
len (длина) — сколько элементов сейчас в срезе.
cap (ёмкость) — сколько элементов может вместить срез без выделения новой памяти.
Представьте себе записную книжку. Вы записали 3 заметки (длина = 3), но в книжке 10 страниц (ёмкость = 10). Пока заметок меньше 10, новые добавляются мгновенно. Когда страницы закончатся, придётся купить новую книжку побольше — Go сделает это автоматически.
package main
import "fmt"
func main() {
// make(тип, длина, ёмкость)
s := make([]int, 3, 10)
fmt.Println(s) // [0 0 0]
fmt.Println(len(s)) // 3
fmt.Println(cap(s)) // 10
}
Здесь мы создали срез длиной 3 и ёмкостью 10. Сейчас в нём 3 элемента, но внутри зарезервировано место под 10. На начальном этапе вам не нужно думать о ёмкости — Go сам её увеличивает. Просто знайте, что она существует.
➕ Добавление элементов: append
Главное преимущество среза перед массивом — можно добавлять элементы. Для этого используется встроенная функция append:
package main
import "fmt"
func main() {
// Начинаем с пустого среза
var fruits []string
// Добавляем элементы по одному
fruits = append(fruits, "яблоко")
fruits = append(fruits, "банан")
fruits = append(fruits, "вишня")
fmt.Println(fruits) // [яблоко банан вишня]
fmt.Println(len(fruits)) // 3
// Можно добавить сразу несколько
fruits = append(fruits, "груша", "апельсин", "киви")
fmt.Println(fruits) // [яблоко банан вишня груша апельсин киви]
fmt.Println(len(fruits)) // 6
}
💡 Подсказка: обратите внимание, что append не изменяет исходный срез, а возвращает новый. Поэтому мы всегда пишем fruits = append(fruits, ...) — результат присваиваем обратно в ту же переменную. Если забыть присвоение, элемент не добавится!
Можно также добавить один срез к другому с помощью оператора ... (троеточие):
a := []int{1, 2, 3}
b := []int{4, 5, 6}
// Добавляем все элементы b к a
a = append(a, b...)
fmt.Println(a) // [1 2 3 4 5 6]
Троеточие ... после b означает: «распакуй этот срез и передай каждый элемент как отдельный аргумент». Без троеточия код не скомпилируется, потому что append ожидает отдельные элементы, а не срез целиком.
✂️ Срез среза (slicing)
Из любого среза (или массива) можно вырезать подмножество элементов. Для этого используется синтаксис [начало:конец]:
package main
import "fmt"
func main() {
numbers := []int{10, 20, 30, 40, 50, 60, 70}
// Элементы с индекса 1 до 4 (не включая 4)
part := numbers[1:4]
fmt.Println(part) // [20 30 40]
// С начала до индекса 3
first := numbers[:3]
fmt.Println(first) // [10 20 30]
// С индекса 4 до конца
last := numbers[4:]
fmt.Println(last) // [50 60 70]
// Полная копия по ссылке
all := numbers[:]
fmt.Println(all) // [10 20 30 40 50 60 70]
}
Правило простое: [a:b] берёт элементы от индекса a (включительно) до индекса b (не включая). Если a не указан, он равен 0. Если b не указан, он равен длине среза.
💡 Подсказка: срез среза не создаёт копию данных. Новый срез ссылается на тот же участок памяти. Если вы измените элемент в part, он изменится и в numbers. Это важно помнить! Если нужна независимая копия, используйте функцию copy.
numbers := []int{10, 20, 30, 40, 50}
part := numbers[1:3] // [20 30]
// Изменяем элемент в part
part[0] = 999
// Изменился и в numbers!
fmt.Println(numbers) // [10 999 30 40 50]
fmt.Println(part) // [999 30]
📋 Копирование среза: copy
Когда нужна независимая копия среза, используйте функцию copy:
package main
import "fmt"
func main() {
original := []int{10, 20, 30, 40, 50}
// Создаём новый срез такой же длины
duplicate := make([]int, len(original))
// Копируем элементы
copy(duplicate, original)
// Теперь изменение одного не влияет на другой
duplicate[0] = 999
fmt.Println(original) // [10 20 30 40 50] — без изменений
fmt.Println(duplicate) // [999 20 30 40 50]
}
Функция copy(куда, откуда) копирует элементы из одного среза в другой. Она возвращает количество скопированных элементов. Важно: целевой срез (duplicate) должен быть уже создан и иметь достаточную длину — copy не расширяет его автоматически.
src := []int{1, 2, 3, 4, 5}
// Срез длины 3 — скопируются только первые 3 элемента
dst := make([]int, 3)
copied := copy(dst, src)
fmt.Println(dst) // [1 2 3]
fmt.Println(copied) // 3
🔄 Итерация через for range
Самый удобный способ пройтись по всем элементам среза — цикл for range. На каждой итерации он даёт два значения: индекс элемента и его значение.
package main
import "fmt"
func main() {
fruits := []string{"яблоко", "банан", "вишня", "груша"}
for index, value := range fruits {
fmt.Printf("Индекс: %d, Значение: %s\n", index, value)
}
}
// Вывод:
// Индекс: 0, Значение: яблоко
// Индекс: 1, Значение: банан
// Индекс: 2, Значение: вишня
// Индекс: 3, Значение: груша
Переменные index и value создаются автоматически на каждом шаге цикла. Вы можете назвать их как угодно: i, v, idx, fruit — на ваш вкус.
Если индекс не нужен, его можно пропустить с помощью символа подчёркивания _:
// Только значения, без индекса
for _, fruit := range fruits {
fmt.Println(fruit)
}
// Только индексы, без значений
for i := range fruits {
fmt.Println(i) // 0, 1, 2, 3
}
Символ _ (пустой идентификатор) говорит Go: «Я знаю, что здесь есть значение, но мне оно не нужно». Без него Go выдаст ошибку «переменная объявлена, но не используется».
Конечно, можно пройтись по срезу и обычным for:
// Классический for — тоже работает
for i := 0; i < len(fruits); i++ {
fmt.Println(fruits[i])
}
Но for range короче, безопаснее (не выйдете за границы массива) и читается лучше. Используйте его по умолчанию.
🧰 Полезные операции со срезами
Несколько типичных приёмов, которые пригодятся на практике:
Удаление элемента по индексу:
package main
import "fmt"
func main() {
nums := []int{10, 20, 30, 40, 50}
// Удаляем элемент с индексом 2 (значение 30)
indexToRemove := 2
nums = append(nums[:indexToRemove], nums[indexToRemove+1:]...)
fmt.Println(nums) // [10 20 40 50]
}
Здесь мы берём всё до удаляемого элемента (nums[:2] = [10, 20]) и всё после него (nums[3:] = [40, 50]), а затем соединяем через append.
Проверка, пуст ли срез:
var items []string
if len(items) == 0 {
fmt.Println("Срез пустой")
}
Подсчёт суммы элементов:
numbers := []int{10, 20, 30, 40, 50}
sum := 0
for _, n := range numbers {
sum += n
}
fmt.Println("Сумма:", sum) // Сумма: 150
Поиск максимального значения:
numbers := []int{42, 17, 93, 8, 56}
max := numbers[0]
for _, n := range numbers {
if n > max {
max = n
}
}
fmt.Println("Максимум:", max) // Максимум: 93
🛠 Практика: сортировка оценок студента
Давайте соберём все знания воедино и напишем полноценную программу. Задача: у нас есть список оценок студента, нужно отсортировать их, посчитать среднюю оценку и вывести результат.
Для сортировки мы реализуем простейший алгоритм — пузырьковую сортировку. Он не самый быстрый, но зато наглядный и отлично подходит для обучения.
package main
import "fmt"
func main() {
// Оценки студента (по 5-балльной шкале)
grades := []int{3, 5, 2, 4, 5, 3, 4, 2, 5, 4}
fmt.Println("Оценки до сортировки:", grades)
fmt.Println("Количество оценок:", len(grades))
// === Пузырьковая сортировка ===
// Идея: проходим по списку и меняем местами соседние
// элементы, если они стоят в неправильном порядке.
// Повторяем, пока всё не встанет на свои места.
n := len(grades)
for i := 0; i < n-1; i++ {
for j := 0; j < n-1-i; j++ {
if grades[j] > grades[j+1] {
// Меняем элементы местами
grades[j], grades[j+1] = grades[j+1], grades[j]
}
}
}
fmt.Println("Оценки после сортировки:", grades)
// === Подсчёт средней оценки ===
sum := 0
for _, g := range grades {
sum += g
}
average := float64(sum) / float64(len(grades))
fmt.Printf("Средняя оценка: %.2f\n", average)
// === Подсчёт каждой оценки ===
// Создаём срез-счётчик для оценок от 1 до 5
// Индекс 0 не используем, чтобы оценка совпадала с индексом
counts := make([]int, 6) // индексы 0-5
for _, g := range grades {
counts[g]++
}
fmt.Println("\nСтатистика оценок:")
for grade := 1; grade <= 5; grade++ {
if counts[grade] > 0 {
fmt.Printf(" Оценка %d: %d раз(а)\n", grade, counts[grade])
}
}
// === Лучшие и худшие оценки ===
// После сортировки первый элемент — минимум, последний — максимум
fmt.Printf("\nМинимальная оценка: %d\n", grades[0])
fmt.Printf("Максимальная оценка: %d\n", grades[len(grades)-1])
}
Запустите эту программу:
go run main.go
Вывод будет таким:
Оценки до сортировки: [3 5 2 4 5 3 4 2 5 4]
Количество оценок: 10
Оценки после сортировки: [2 2 3 3 4 4 4 5 5 5]
Средняя оценка: 3.70
Статистика оценок:
Оценка 2: 2 раз(а)
Оценка 3: 2 раз(а)
Оценка 4: 3 раз(а)
Оценка 5: 3 раз(а)
Минимальная оценка: 2
Максимальная оценка: 5
Разберём ключевые моменты:
Пузырьковая сортировка: два вложенных цикла. Внешний (i) определяет, сколько проходов нужно. Внутренний (j) сравнивает соседние элементы и меняет их местами, если левый больше правого. С каждым проходом самый большой элемент «всплывает» в конец — отсюда и название «пузырьковая».
Обмен переменных: строка grades[j], grades[j+1] = grades[j+1], grades[j] — это красивая фишка Go. Можно менять значения двух переменных местами без временной переменной. В других языках пришлось бы писать temp = a; a = b; b = temp.
Средняя оценка: делим сумму на количество. Преобразование float64(sum) необходимо, потому что деление двух целых чисел в Go даёт целое число (без дробной части). Чтобы получить 3.70, а не 3, нужно привести хотя бы одно из чисел к float64.
Подсчёт оценок: трюк с counts[g]++ — используем значение оценки как индекс в срезе-счётчике. Оценка 5 увеличивает counts[5], оценка 3 — counts[3]. Это частый паттерн в программировании.
💡 Подсказка: в реальных проектах для сортировки используют пакет sort из стандартной библиотеки: sort.Ints(grades) — и готово. Но понимать, как сортировка работает изнутри, очень полезно. Мы вернёмся к пакету sort в будущих уроках.
⚖️ Массив vs Срез — когда что использовать
Подведём итог по различиям:
Массив ([N]T):
Фиксированный размер. Размер — часть типа. При передаче в функцию копируется. Используется редко.
Срез ([]T):
Динамический размер. Растёт через append. Передаётся по ссылке (лёгкий). Используется почти всегда.
Простое правило: если не уверены — используйте срез. В 99% случаев это правильный выбор. Массивы нужны только в специфичных ситуациях, например при работе с криптографией или когда размер гарантированно фиксирован (RGB-цвет из 3 компонентов, координаты точки).
📋 Итоги урока
- Массив (
[N]int) — фиксированный размер, задаётся при создании, изменить нельзя - Срез (
[]int) — динамический размер, может расти и уменьшаться; используется в 99% случаев make([]int, len, cap)— создаёт срез заданной длины и ёмкостиappend(slice, elem)— добавляет элементы; результат нужно присваивать обратноlen()иcap()— длина и ёмкость срезаslice[a:b]— вырезает подсрез от индексаaдоb(не включая)copy(dst, src)— создаёт независимую копию данныхfor index, value := range slice— самый удобный способ перебора элементов- Мы написали программу сортировки оценок студента с подсчётом статистики
В следующем уроке мы познакомимся с map — словарями (хэш-таблицами). Если срез хранит элементы по числовому индексу (0, 1, 2...), то map позволяет использовать любой ключ: строку, число, что угодно. Хотите найти телефон по имени, цену по названию товара, перевод по слову? Для этого и нужен map. Это одна из самых используемых структур данных в программировании!