Golang: Срезы
Срезы (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Можно создать пустой срез, добавив в конце {}:
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().
Передача и возврат из функции
Когда срез передаётся в функцию, копируется только небольшой «заголовок» среза: длина, вместимость и указатель на общий подлежащий массив. Сами элементы не копируются. Поэтому если внутри функции менять элементы по индексу (s[i] = ...), изменения видны и снаружи. Это остаётся передачей по значению (копируется заголовок), просто в нём хранится указатель на данные.
Также можно возвращать срез из функции:
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]
}Подробнее о нюансах append() при работе внутри функций (и почему иногда изменения «не выходят» наружу) узнаем в одном из следующих уроков.
Пример: заполнение среза в цикле и возврат
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() | Да | Да |
| Используется в практике | Редко | Да (везде) |
Задание
Реализуйте функцию AddDiscount(prices []float64, discount float64) []float64, которая применяет скидку к последнему элементу среза цен. Если передан пустой срез, функция должна вернуть пустой срез.
Правила:
- Скидка передаётся в процентах, например
10означает 10% скидки.
Примеры:
// 300 - 300 * 0.1
AddDiscount([]float64{100, 200, 300}, 10) // [100 200 270]
AddDiscount([]float64{}, 10) // []
AddDiscount([]float64{500}, 20) // [400]Полезное
Golang: Срезы
Срезы (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Можно создать пустой срез, добавив в конце {}:
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().
Передача и возврат из функции
Когда срез передаётся в функцию, копируется только небольшой «заголовок» среза: длина, вместимость и указатель на общий подлежащий массив. Сами элементы не копируются. Поэтому если внутри функции менять элементы по индексу (s[i] = ...), изменения видны и снаружи. Это остаётся передачей по значению (копируется заголовок), просто в нём хранится указатель на данные.
Также можно возвращать срез из функции:
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]
}Подробнее о нюансах append() при работе внутри функций (и почему иногда изменения «не выходят» наружу) узнаем в одном из следующих уроков.
Пример: заполнение среза в цикле и возврат
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() | Да | Да |
| Используется в практике | Редко | Да (везде) |
Задание
Реализуйте функцию AddDiscount(prices []float64, discount float64) []float64, которая применяет скидку к последнему элементу среза цен. Если передан пустой срез, функция должна вернуть пустой срез.
Правила:
- Скидка передаётся в процентах, например
10означает 10% скидки.
Примеры:
// 300 - 300 * 0.1
AddDiscount([]float64{100, 200, 300}, 10) // [100 200 270]
AddDiscount([]float64{}, 10) // []
AddDiscount([]float64{500}, 20) // [400]Полезное
Ваше упражнение проверяется по этим тестам
package solution
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestAddDiscount(t *testing.T) {
a := assert.New(t)
a.Equal([]float64{100, 200, 270}, AddDiscount([]float64{100, 200, 300}, 10))
a.Equal([]float64{}, AddDiscount([]float64{}, 15))
a.Equal([]float64{400}, AddDiscount([]float64{500}, 20))
a.Equal([]float64{50, 90}, AddDiscount([]float64{50, 100}, 10))
}Решение учителя откроется через:
20:00
