Skip to content

Срезы в Go

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

Массивы

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

TIP

Массивы в Go передаются по значению. При передаче массива в функцию он копируется целиком.

Инициализация

Длина массива должна быть константой, а не переменной:

go
// Правильный пример
var a [5]int

// Неправильный пример
l := 1
var b [l]int

Инициализация массива длиной 5:

go
var nums [5]int

Инициализация элементами:

go
nums := [5]int{1, 2, 3}

Автоматическое определение длины компилятором:

go
nums := [...]int{1, 2, 3, 4, 5} // эквивалентно nums := [5]int{1, 2, 3, 4, 5}, троеточие обязательно, иначе будет срез

Получение указателя через new:

go
nums := new([5]int)

Все способы выделяют фиксированную память, разница лишь в том, что последний возвращает указатель.

Важно: длина должна быть константным выражением, иначе компиляция не пройдёт:

go
length := 5 // переменная
var nums [length]int // ошибка

Правильные примеры:

go
const length = 5
var nums [length]int // константа
var nums2 [length + 1]int // константное выражение
var nums3 [(1 + 2 + 3) * 5]int // константное выражение
var nums4 [5]int // наиболее часто используется

Использование

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

go
fmt.Println(nums[0])

Изменение элементов:

go
nums[0] = 1

Получение количества элементов через len:

go
len(nums)

Получение ёмкости через cap (для массивов ёмкость равна длине):

go
cap(nums)

Нарезка

Формат нарезки: arr[startIndex:endIndex], интервал левый закрытый, правый открытый. После нарезки массив становится срезом:

go
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]
go
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

Преобразование массива в срез без параметров:

go
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:

go
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]

Срезы

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

Инициализация

Способы инициализации среза:

go
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 добавляет элементы:

go
func append(slice []Type, elems ...Type) []Type

Пример:

go
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

Вставка элементов

Исходный срез:

go
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

Вставка в начало:

go
nums = append([]int{-1, 0}, nums...)
fmt.Println(nums) // [-1 0 1 2 3 4 5 6 7 8 9 10]

Вставка в середину (индекс i):

go
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]

Вставка в конец:

go
nums = append(nums, 99, 100)
fmt.Println(nums) // [1 2 3 4 5 6 7 8 9 10 99 100]

Удаление элементов

Удаление с начала (n элементов):

go
nums = nums[n:]
fmt.Println(nums) // n=3, [4 5 6 7 8 9 10]

Удаление с конца (n элементов):

go
nums = nums[:len(nums)-n]
fmt.Println(nums) // n=3, [1 2 3 4 5 6 7]

Удаление из середины (с индекса i, n элементов):

go
nums = append(nums[:i], nums[i+n:]...)
fmt.Println(nums) // i=2, n=3, [1 2 6 7 8 9 10]

Очистка всех элементов:

go
nums = nums[:0]
fmt.Println(nums) // []

Копирование

Целевой срез должен иметь достаточную длину:

go
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:

go
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:

go
func main() {
  slice := []int{1, 2, 3, 4, 5, 7, 8, 9}
  for index, val := range slice {
    fmt.Println(index, val)
  }
}

Многомерные срезы

Пример из официальной документации: Effective Go - Двумерные срезы

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]

[]
[]
[]
[]
[]

Массивы инициализируются полностью, а срезы требуют отдельной инициализации:

go
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):

go
slice[low:high:max]

Требование: low <= high <= max <= cap. Ёмкость результата: max - low.

Пример без max:

go
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:

go
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]

Решение с расширенным выражением:

go
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 для обнуления элементов среза:

go
package main

import (
    "fmt"
)

func main() {
    s := []int{1, 2, 3, 4}
    clear(s)
    fmt.Println(s)
}

Вывод:

[0 0 0 0]

Для полной очистки среза:

go
func main() {
  s := []int{1, 2, 3, 4}
    s = s[:0:0]
  fmt.Println(s)
}

Ограничение ёмкости предотвращает перезапись последующих элементов.

Golang by www.golangdev.cn edit