Срезы
Срезы (slice) это дескриптор (структура), которая указывает на сегмент базового массива. В отличие от массивов длина в срезах не фиксирована и динамически может меняться, то есть можно добавлять новые элементы или удалять уже существующие.
Срез сам по себе ничего не хранит. Это просто ссылка, которая смотрит на реальный массив в памяти. У него есть три важные характеристики:
- Указатель: на то место в массиве, где начинается срез.
- Длина (length): сколько элементов в нем сейчас.
- Емкость (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
}