Срезы в Go
В Go массивы и срезы выглядят почти одинаково, но имеют существенные различия. Массивы имеют фиксированную длину, которая не может быть изменена, а срезы — динамические, они автоматически увеличивают ёмкость при необходимости.
Массивы
Если известна длина данных и не требуется изменение размера, можно использовать массив. В Go массивы — значения, а не ссылки, они не являются указателями на первый элемент.
TIP
Массивы в Go передаются по значению. При передаче массива в функцию он копируется целиком.
Инициализация
Длина массива должна быть константой, а не переменной:
// Правильный пример
var a [5]int
// Неправильный пример
l := 1
var b [l]intИнициализация массива длиной 5:
var nums [5]intИнициализация элементами:
nums := [5]int{1, 2, 3}Автоматическое определение длины компилятором:
nums := [...]int{1, 2, 3, 4, 5} // эквивалентно nums := [5]int{1, 2, 3, 4, 5}, троеточие обязательно, иначе будет срезПолучение указателя через new:
nums := new([5]int)Все способы выделяют фиксированную память, разница лишь в том, что последний возвращает указатель.
Важно: длина должна быть константным выражением, иначе компиляция не пройдёт:
length := 5 // переменная
var nums [length]int // ошибкаПравильные примеры:
const length = 5
var nums [length]int // константа
var nums2 [length + 1]int // константное выражение
var nums3 [(1 + 2 + 3) * 5]int // константное выражение
var nums4 [5]int // наиболее часто используетсяИспользование
Доступ к элементам по индексу:
fmt.Println(nums[0])Изменение элементов:
nums[0] = 1Получение количества элементов через len:
len(nums)Получение ёмкости через cap (для массивов ёмкость равна длине):
cap(nums)Нарезка
Формат нарезки: arr[startIndex:endIndex], интервал левый закрытый, правый открытый. После нарезки массив становится срезом:
nums := [5]int{1, 2, 3, 4, 5}
nums[:] // [0,5) -> [1 2 3 4 5]
nums[1:] // [1,5) -> [2 3 4 5]
nums[:5] // [0,5) -> [1 2 3 4 5]
nums[2:3] // [2,3) -> [3]
nums[1:3] // [1,3) -> [2 3]func main() {
arr := [5]int{1, 2, 3, 4, 5}
fmt.Printf("%T\n", arr)
fmt.Printf("%T\n", arr[1:2])
}Вывод:
[5]int
[]intПреобразование массива в срез без параметров:
func main() {
arr := [5]int{1, 2, 3, 4, 5}
slice := arr[:]
slice[0] = 0
fmt.Printf("array: %v\n", arr)
fmt.Printf("slice: %v\n", slice)
}Вывод:
array: [0 2 3 4 5]
slice: [0 2 3 4 5]Для независимого среза используйте slices.Clone:
func main() {
arr := [5]int{1, 2, 3, 4, 5}
slice := slices.Clone(arr[:])
slice[0] = 0
fmt.Printf("array: %v\n", arr)
fmt.Printf("slice: %v\n", slice)
}Вывод:
array: [1 2 3 4 5]
slice: [0 2 3 4 5]Срезы
Срезы используются гораздо чаще массивов для хранения данных неизвестной длины с возможными вставками и удалениями.
Инициализация
Способы инициализации среза:
var nums []int // значение
nums := []int{1, 2, 3} // значение
nums := make([]int, 0, 0) // значение
nums := new([]int) // указательРазница с массивами — отсутствие указания длины. Рекомендуется использовать make с предварительным выделением ёмкости для уменьшения перераспределений.
make принимает три параметра: тип, длина, ёмкость. Длина — количество элементов, ёмкость — максимальное количество элементов. Ёмкость среза может автоматически увеличиваться, в отличие от массивов. Подробнее: Справочник - Длина и ёмкость.
TIP
Срезы — ссылочный тип, внутренне представляют структуру с указателем на массив, длиной и ёмкостью. При передаче в функцию массив не копируется, изменения внутри функции влияют на исходный срез.
var nums []int создаёт nil срез без выделения памяти. make рекомендуется использовать с достаточной ёмкостью.
Использование
Использование срезов аналогично массивам, но с динамической длиной.
Функция append добавляет элементы:
func append(slice []Type, elems ...Type) []TypeПример:
nums := make([]int, 0, 0)
nums = append(nums, 1, 2, 3, 4, 5, 6, 7)
fmt.Println(len(nums), cap(nums)) // 7 8Стратегия расширения срезов изменилась в Go 1.18:
- До 1.18: ёмкость < 1024 → удвоение, иначе ×1.25
- После 1.18: ёмкость < 256 → удвоение, иначе
newcap = oldcap + (oldcap + 3*256)/4
Вставка элементов
Исходный срез:
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}Вставка в начало:
nums = append([]int{-1, 0}, nums...)
fmt.Println(nums) // [-1 0 1 2 3 4 5 6 7 8 9 10]Вставка в середину (индекс i):
nums = append(nums[:i+1], append([]int{999, 999}, nums[i+1:]...)...)
fmt.Println(nums) // i=3, [1 2 3 4 999 999 5 6 7 8 9 10]Вставка в конец:
nums = append(nums, 99, 100)
fmt.Println(nums) // [1 2 3 4 5 6 7 8 9 10 99 100]Удаление элементов
Удаление с начала (n элементов):
nums = nums[n:]
fmt.Println(nums) // n=3, [4 5 6 7 8 9 10]Удаление с конца (n элементов):
nums = nums[:len(nums)-n]
fmt.Println(nums) // n=3, [1 2 3 4 5 6 7]Удаление из середины (с индекса i, n элементов):
nums = append(nums[:i], nums[i+n:]...)
fmt.Println(nums) // i=2, n=3, [1 2 6 7 8 9 10]Очистка всех элементов:
nums = nums[:0]
fmt.Println(nums) // []Копирование
Целевой срез должен иметь достаточную длину:
func main() {
dest := make([]int, 0)
src := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Println(src, dest)
fmt.Println(copy(dest, src))
fmt.Println(src, dest)
}Вывод:
[1 2 3 4 5 6 7 8 9] []
0
[1 2 3 4 5 6 7 8 9] []С длиной 10:
[1 2 3 4 5 6 7 8 9] [0 0 0 0 0 0 0 0 0 0]
9
[1 2 3 4 5 6 7 8 9] [1 2 3 4 5 6 7 8 9 0]Обход
Цикл for:
func main() {
slice := []int{1, 2, 3, 4, 5, 7, 8, 9}
for i := 0; i < len(slice); i++ {
fmt.Println(slice[i])
}
}Цикл for range:
func main() {
slice := []int{1, 2, 3, 4, 5, 7, 8, 9}
for index, val := range slice {
fmt.Println(index, val)
}
}Многомерные срезы
Пример из официальной документации: Effective Go - Двумерные срезы
var nums [5][5]int
for _, num := range nums {
fmt.Println(num)
}
fmt.Println()
slices := make([][]int, 5)
for _, slice := range slices {
fmt.Println(slice)
}Вывод:
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[]
[]
[]
[]
[]Массивы инициализируются полностью, а срезы требуют отдельной инициализации:
slices := make([][]int, 5)
for i := 0; i < len(slices); i++ {
slices[i] = make([]int, 5)
}Вывод:
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]Расширенное выражение
TIP
Расширенное выражение доступно только для срезов.
Формат (добавлен в Go 1.2):
slice[low:high:max]Требование: low <= high <= max <= cap. Ёмкость результата: max - low.
Пример без max:
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
s2 := s1[3:4] // cap = 9 - 3 = 6Проблема: s1 и s2共享底层数组,修改 s2 влияет на s1:
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
s2 := s1[3:4]
s2 = append(s2, 1)
fmt.Println(s2)
fmt.Println(s1)Вывод:
[4 1]
[1 2 3 4 1 6 7 8 9]Решение с расширенным выражением:
func main() {
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
s2 := s1[3:4:4] // cap = 4 - 3 = 1
s2 = append(s2, 1) // ёмкость不足,分配新数组
fmt.Println(s2)
fmt.Println(s1)
}Вывод:
[4 1]
[1 2 3 4 5 6 7 8 9]clear
В Go 1.21 добавлена функция clear для обнуления элементов среза:
package main
import (
"fmt"
)
func main() {
s := []int{1, 2, 3, 4}
clear(s)
fmt.Println(s)
}Вывод:
[0 0 0 0]Для полной очистки среза:
func main() {
s := []int{1, 2, 3, 4}
s = s[:0:0]
fmt.Println(s)
}Ограничение ёмкости предотвращает перезапись последующих элементов.
