Перейти к основному содержимому

Срезы

Срезы (slice) это дескриптор (структура), которая указывает на сегмент базового массива. В отличие от массивов длина в срезах не фиксирована и динамически может меняться, то есть можно добавлять новые элементы или удалять уже существующие.

Срез сам по себе ничего не хранит. Это просто ссылка, которая смотрит на реальный массив в памяти. У него есть три важные характеристики:

  1. Указатель: на то место в массиве, где начинается срез.
  2. Длина (length): сколько элементов в нем сейчас.
  3. Емкость (capacity): сколько элементов в него может поместиться, прежде чем ему станет тесно.

Срез определяется также, как и массив, за тем исключением, что у него не указывается длина:

var users []string

Также срез можно инициализировать значениями:

var users = []string{"Tom", "Alice", "Kate"}
// или так
users2 := []string{"Tom", "Alice", "Kate"}

Оператор среза и создания среза из последовательностей

Также можно создать срез из другой последовательности элементов, например, из массива. Для этого применяется оператор среза - : (двоеточие)

s[i:j]

Этому оператору нужно указать два индекса: начальный (i) и конечный (j) индексы массива, все элементы между которыми попадут в срез. Оператор среза создает из последовательности (например, из массива) s новый срез, который содержит элементы последовательности s с i по j-1. При этом должно соблюдаться условие 0 <= i <= j <= cap(s). В качестве исходной последовательности, из которой берутся элементы, может использоваться массив, указатель на массив или другой срез. В итоге в полученном срезе будет j-i элементов.

Если начальный индекс не указан, то по умолчанию применяется значение 0. Если конечный индекс не указан, то вместо него используется длина исходной последовательности s.

// исходный массив
initialUsers := [8]string{"Bob", "Alice", "Kate", "Sam", "Tom", "Paul", "Mike", "Robert"}
users1 := initialUsers[2:6] // с 3-го по 6-й
users2 := initialUsers[:4] // с 1-го по 4-й
users3 := initialUsers[3:] // с 4-го до конца

fmt.Println(users1) // ["Kate", "Sam", "Tom", "Paul"]
fmt.Println(users2) // ["Bob", "Alice", "Kate", "Sam"]
fmt.Println(users3) // ["Sam", "Tom", "Paul", "Mike", "Robert"]

Cоздание среза из строки:

package main
import "fmt"

func main() {

str := "Hello World"
slice := str[6:]

fmt.Println(slice) // World
}

Создание среза из другого среза

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

package main
import "fmt"

func main() {

var array [7] int = [7] int {1, 2, 3, 4, 5, 6, 7}

slice1 := array[1:6]
fmt.Println("slice from array:", slice1) // slice from array: [2 3 4 5 6]

slice2 := slice1[:3]
fmt.Println("slice2 from slice1:", slice2) // slice2 from slice1: [2 3 4]
}

Обращение к элементам среза и перебор среза

К элементам среза обращение происходит также, как и к элементам массива - по индексу и также мы можем перебирать все элементы с помощью цикла for:

var users []string = []string{"Tom", "Alice", "Kate"}
fmt.Println(users[2]) // Kate
users[2] = "Katherine"

for _, value := range users{
fmt.Println(value)

Внутренняя реализация среза

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

Указатель В реальности срезы не хранят никаких данных: они лишь ссылаются на массив или раздел базового массива. Инициализация среза из массива и изменение элементов массива также изменяют значение среза.Аналогично, изменение значения элемента среза отражает его ссылаемый массив. Например:

package main
import "fmt"

func main() {

array := [6] int {1, 2, 3, 4, 5, 6}
fmt.Println("Начальный массив:", array) // [1 2 3 4 5 6]

slice := array[1:5]
fmt.Println("Начальный срез:", slice) // [2 3 4 5]

// изменяем второй элемент массива
array[1] = 22
// также меняется и срез
fmt.Println("Срез после изменения в массиве:", slice) // [22 3 4 5]


// изменяем второй элемент среза
slice[1] = 33
// также меняется и массив
fmt.Println("Массив после изменения в срезе:", array) // [1 22 33 4 5 6]

}

Длина (Length)

Длина — это количество элементов, на которые ссылается срез. Для получения длины среза применяется функция len(). Она возвращает общее количество элементов, которые содержит срез. Доступ к элементам за пределами длины (даже если они входят в емкость) вызовет panic (index out of range).

package main
import "fmt"

func main() {

array := [6] int {1, 2, 3, 4, 5, 6}
slice := array[1:5]
fmt.Println("Длина среза:", len(slice)) // 4
}

Емкость (Capacity)

Емкость — это количество элементов в базовом массиве, начиная с первого элемента слайса. Длина связана со срезом, в то время как емкость связана с массивом. Это означает, что емкость среза относится к размеру, до которого может быть расширен срез. Для получения емкости применяется функция cap():

package main
import "fmt"

func main() {

array := [6] int {1, 2, 3, 4, 5, 6}
slice := array[1:5]
fmt.Println("Емкость среза:", cap(slice)) // 5
}

В данном случае срез начинается со второго элемента массива, в котором всего 6 элементов. Соответственно емкость - 5 элементов.

Реаллокация (Grow)

Когда при использовании append длина слайса должна превысить его емкость:

  • Go выделяет в памяти новый, более вместительный массив.
  • Копирует данные из старого массива в новый.
  • Слайс обновляет свой указатель на новый массив, а cap увеличивается.

Обычно емкость удваивается, если слайс небольшой. Для очень больших слайсов коэффициент роста меньше (в современных версиях Go формула сложнее, чтобы переход был плавным, но обычно говорят про коэффициент ~1.25 для больших объемов).

Создание среза с помощью функции make

Еще один способ создания среза представляет функция make() - она позволяет создать срез из нескольких элементов, которые будут иметь значения по умолчанию. Она имеет следующую форму:

slice_name := make([] тип_элементов_среза, длина_среза, емкость_среза)

Эта функция как раз позволяет задать дину и емкость среза. Пример использования:

var users []string = make([]string, 3) // длина среза - 3
users[0] = "Tom"
users[1] = "Alice"
users[2] = "Bob"

Двухмерные срезы

Подобно массивам, мы также можем создавать двухмерные срезы. Такие срезы могут быть определены как срезы, которые, в свою очередь, содержат другие срезы:

slice2D := [][] int{
[] int {1, 2},
[] int {3, 4},
[] int {5, 6},
}

Добавление в срез

Для добавления в срез применяется встроенная функция append(slice, value). Первый параметр функции - срез, в который надо добавить, а второй параметр - значение, которое нужно добавить. Результатом функции является увеличенный срез.

users := []string{"Tom", "Alice", "Kate"}
users = append(users, "Bob")

fmt.Println(users) // [Tom Alice Kate Bob]

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

В случае когда необходимо удалить какой то определённый элемент мы можем комбинировать функцию append и оператор среза:

users := []string{"Bob", "Alice", "Kate", "Sam", "Tom", "Paul", "Mike", "Robert"}
//удаляем 4-й элемент
var n = 3
users = append(users[:n], users[n+1:]...)
fmt.Println(users) //["Bob", "Alice", "Kate", "Tom", "Paul", "Mike", "Robert"]

Копирование среза

Копирование среза производится с помощью функции copy():

func copy( destination, source [] T) int

Эта функция принимает два аргумента: исходный срез, который мы хотим скопировать, и целевой срез, в который мы хотим скопировать элементы. Она всегда возвращает целочисленное число, которое представляет количество скопированных элементов. Возвращаемое значение всегда является минимальной длиной исходного среза и целевого среза. Применение:

package main
import "fmt"

func main() {

slice1 := [] int {1, 2, 3, 4, 5, 6}
slice2 := [] int {}
copy(slice2, slice1) // копируем из slice2 в slice2
fmt.Println(slice2) // [] - slice2 пуст
}

Здесь после копирования срез slice2 также, как и при создании пустой. Так как его начальная длина 0, соответственно копируется 0 элементов. Другой пример:

slice1 := [] int {1, 2, 3, 4, 5, 6}
slice2 := make([]int, 3)
copy(slice2, slice1) // копируем из slice2 в slice2
fmt.Println(slice2) // [1 2 3]

Здесь длина среза slice2 - 3, поэтому копируется 3 элемента.

slice1 := [] int {1, 2, 3, 4, 5, 6}
slice2 := make([]int, 8)
copy(slice2, slice1) // копируем из slice2 в slice2
fmt.Println(slice2) // [1 2 3 4 5 6 0 0]

Здесь длина slice2 больше, чем у среза slice1, поэтому копируются все элементы из slice1, а остальные элементы в slice2 оставляют значения, которые были до копирования (в данном случае значения по умолчанию - 0).

Сортировка среза

В Go есть пакет sort, который предоставляет несколько методов сортировки для среза типов int, float64 или string. Для этого пакет sort предоставляет соответственно функции sort.Ints(), sort.Float64s() и sort.Strings. Эти методы сортировки всегда сортируют срез в порядке возрастания:

package main
import (
"fmt"
"sort" //подключаем пакет sort
)

func main() {

users := []string{"Tom", "Bob", "Sam"}
sort.Strings(users)

fmt.Println(users) // [Bob Sam Tom]
}

Поиск в срезе

Go также для срезов типов int, float64 или string позволяет узнать, существует ли определенный элемент в срезе или нет. Для этого пакет sort предоставляет соответственно функции sort.SearchInts(), sort.SearchFloat64s() и sort.SearchStrings. Эти функции выполняют двоичный поиск в отсортированном срезе в порядке возрастания. Если элемент найден в отсортированном срезе, функция поиска возвращает его индекс; если нет, она возвращает индекс, по которому элемент будет помещен в срез:

package main
import (
"fmt"
"sort"
)

func main() {

intSlice := [] int {11, 22, 33, 44, 55, 66}
// ищем число 33
fmt.Println(sort.SearchInts(intSlice, 33)) // 2
// ищем число 45
fmt.Println(sort.SearchInts(intSlice, 45)) // 4

stringSlice := []string{"Tom", "Bob", "Sam"}
// ищем строку "Sam"
fmt.Println(sort.SearchStrings(stringSlice, "Sam")) // 2 - после автосортировки ["Bob", "Sam", "Tom"]
// ищем строку "Alice"
fmt.Println(sort.SearchStrings(stringSlice, "Alice")) // 0
}

Срез в обратном порядке

Функция reverse возвращает срез в обратном/убывающем порядке. Она хорошо работает с отсортированными или несортированными срезами. Но перед переворачиванием среза, его надо обернуть в тип IntSlice/Float64Slice/StringSlice с помощью соответственно функций sort.IntSlice/sort.Float64Slice/sort.StringSlice:

package main
import (
"fmt"
"sort"
)

func main() {

intSlice := [] int {11, 22, 33, 44, 55, 66}
sort.Sort(sort.Reverse(sort.IntSlice(intSlice)))
fmt.Println(intSlice) // [66 55 44 33 22 11]

stringSlice := []string{"Tom", "Bob", "Sam"}
sort.Sort(sort.Reverse(sort.StringSlice(stringSlice)))
fmt.Println(stringSlice) // [Tom Sam Bob]
}

Сравнение срезов

Два среза считаются равными, если они хранят данные одного типа и содержат одни и те же элементы. Go предоставляет функцию DeepEqual() для сравнения срезов, равны они или нет. Мы можем получить доступ к этой функции, используя пакет reflect:

package main
import (
"fmt"
"reflect" // для сравнения срезов
)

func main() {

slice1 := [] int{1, 2, 3, 4}
slice2 := []string{"Tom", "Bob", "Sam"}
slice3 := [] int{1, 2, 3}
slice4 := [] int{1, 2, 3, 4}

fmt.Println("slice1 == slice2:", reflect.DeepEqual(slice1, slice2)) // false
fmt.Println("slice1 == slice3:", reflect.DeepEqual(slice1, slice3)) // false
fmt.Println("slice1 == slice4:", reflect. DeepEqual(slice1, slice4)) // true
}