Зарегистрируйтесь, чтобы продолжить обучение

Срезы Основы Go

Срезы (slices) — это основной способ работы с коллекциями переменной длины в Go. В отличие от массивов, срезы не фиксируют количество элементов в типе и позволяют изменять длину коллекции. По сути это аналог списков из других языков.

Технически срез — это обёртка над массивом. Он хранит в себе три вещи:

  • Указатель на первый элемент.
  • Текущую длину (len()).
  • Вместимость (cap()) — максимальное количество элементов, которые можно хранить, не выделяя новый массив.

Объявление среза

Объявление среза аналогично массиву, с одним исключением. Внутри скобок ничего не указывается, так как длина не известна и может меняться.

var nums []int

Значением по умолчанию в таком определении nums будет nil.

Срез можно создать напрямую с помощью литерала. В этом случае указывается тип элементов, а длина выводится из количества значений:

numbers := []int{10, 20, 30}
fmt.Println(numbers)        // => [10 20 30]
fmt.Println(len(numbers)) // => 3

Можно создать пустой срез добавив в конце {}:

var empty []string{}
fmt.Println(empty)        // => []
fmt.Println(len(empty))   // => 0

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

Такая запись (с добавлением {} в конце) полезна, когда нужно объявить срез заранее, но значения будут добавлены позже (например, в цикле или через append).

Создание среза через make

Функция make() используется для создания срезов с заранее заданной длиной и (опционально) вместимостью. Это бывает полезно для предварительного выделения памяти и улучшения производительности.

nums := make([]int, 5) // длина = 5, вместимость = 5
fmt.Println(nums)        // => [0 0 0 0 0]

Если вы знаете, что срез будет расширяться, но хотите избежать лишних перераспределений памяти, можно задать вместимость больше длины:

buffer := make([]int, 0, 1000) // длина 0, вместимость 1000
fmt.Println(len(buffer))       // => 0
fmt.Println(cap(buffer))       // => 1000

Это позволяет избежать постоянного выделения новой памяти при каждом append() — важно в циклах или при работе с большими объёмами данных. С другой стороны, подобные оптимизации нужны далеко не всегда и заниматься ими имеет смысл только тогда, когда эта часть кода уже стала узким горлышком.

Добавление элементов

Для добавления элементов к срезу используется встроенная функция append(). Она возвращает новый срез с добавленными элементами:

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

Если вместимость позволяет, append() просто дописывает значение. Если нет — Go выделяет новый массив с увеличенной вместимостью.

Можно добавить сразу несколько элементов:

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

Можно объединять срезы:

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

Это удобно, например, при объединении результатов нескольких запросов или списков.

Доступ к элементам

Срезы поддерживают доступ по индексу. В этом смысле поведение идентично массивам:

nums := []int{5, 6, 7}
fmt.Println(nums[1]) // 6

nums[2] = 100
fmt.Println(nums) // [5 6 100]

Если обратиться по индексу, которого нет в срезе — будет ошибка во время выполнения (runtime panic), а не на этапе компиляции.

words := []string{"Go", "Rust"}
fmt.Println(words[2]) // panic: runtime error: index out of range

Поэтому перед обращением стоит проверять длину через len().

Передача и возврат из функции

Срезы передаются в функцию по значению, но это значение — структура, содержащая указатель на массив. То есть сами данные не копируются. Поэтому изменения внутри функции отражаются на оригинальных данных.

Также можно возвращать срез из функции:

func changeAndReturn(s []int) []int {
    s[0] = 999
    return s
}

func main() {
    data := []int{1, 2, 3}
    result := changeAndReturn(data)
    fmt.Println(result) // => [999 2 3]
    // изменения видны и в оригинале
    fmt.Println(data)   // => [999 2 3]
}

Пример: заполнение среза в цикле и возврат

func squares(n int) []int {
    nums := []int{}
    for i := 1; i <= n; i++ {
        nums = append(nums, i * i)
    }
    return nums
}

func main() {
    result := squares(5)
    fmt.Println(result) // => [1 4 9 16 25]
}

Сравнение с массивами

Массив [n]T Срез []T
Фиксированная длина Да Нет
Размер входит в тип Да Нет
Добавление элементов Нет Да (append)
Передача в функцию Полная копия Копия структуры, но не данных
Поддержка len() Да Да
Используется в практике Редко Да (везде)

Дополнительные материалы

  1. Slice types
  2. Effective Go — Slices types

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff