В предыдущем разделе мы рассмотрели массивы в Go. Однако у массивов есть ограничения: после объявления и инициализации их длина не может быть изменена. Поэтому массивы не широко используются в повседневном программировании. В отличие от них, срезы (slices) используются чаще и представляют более гибкую структуру данных.
Точки знания:
Определение среза
Инициализация среза
Операции над срезами, то есть добавление, удаление, изменение и поиск
Расширение среза
Усечение среза
Многомерные срезы
Skills Graph
%%%%{init: {'theme':'neutral'}}%%%%
flowchart RL
go(("Golang")) -.-> go/BasicsGroup(["Basics"])
go(("Golang")) -.-> go/DataTypesandStructuresGroup(["Data Types and Structures"])
go(("Golang")) -.-> go/FunctionsandControlFlowGroup(["Functions and Control Flow"])
go/BasicsGroup -.-> go/values("Values")
go/BasicsGroup -.-> go/variables("Variables")
go/DataTypesandStructuresGroup -.-> go/arrays("Arrays")
go/DataTypesandStructuresGroup -.-> go/slices("Slices")
go/DataTypesandStructuresGroup -.-> go/pointers("Pointers")
go/FunctionsandControlFlowGroup -.-> go/for("For")
subgraph Lab Skills
go/values -.-> lab-149077{{"Структуры данных срезов (Slices) в Golang"}}
go/variables -.-> lab-149077{{"Структуры данных срезов (Slices) в Golang"}}
go/arrays -.-> lab-149077{{"Структуры данных срезов (Slices) в Golang"}}
go/slices -.-> lab-149077{{"Структуры данных срезов (Slices) в Golang"}}
go/pointers -.-> lab-149077{{"Структуры данных срезов (Slices) в Golang"}}
go/for -.-> lab-149077{{"Структуры данных срезов (Slices) в Golang"}}
end
Что такое срез (Slice)
Срезы (slices) похожи на массивы; они являются контейнерами, которые хранят элементы одного и того же типа данных. Однако у массивов есть ограничения: после объявления и инициализации их длина не может быть изменена. Хотя массивы имеют свои области применения, они менее гибкие. Поэтому срезы чаще используются в повседневном программировании.
В Go срезы реализуются с использованием массивов. Срез по сути представляет собой динамический массив, длина которого может изменяться. Мы можем выполнять такие операции, как добавление, удаление, изменение и поиск элементов в срезе, что невозможно сделать с массивом.
Определение среза (Slice)
Синтаксис инициализации среза очень похож на синтаксис инициализации массива. Основное отличие заключается в том, что не нужно указывать длину элементов. Давайте посмотрим на следующий код:
// Declare an array with a length of 5
var a1 [5]byte
// Declare a slice
var s1 []byte
Срез (Slice) - это ссылка на массив
Если мы объявляем массив типа int, нулевое значение каждого элемента массива будет равно 0.
Однако, если мы объявляем срез (slice), нулевое значение среза будет nil. Давайте создадим файл с именем slice.go, чтобы проверить это:
touch ~/project/slice.go
Введите следующий код:
package main
import "fmt"
func main() {
var a [3]int
var s []int
fmt.Println(a[0] == 0) // true
fmt.Println(s == nil) // true
}
go run slice.go
После выполнения кода будет выведен следующий результат:
true
true
Мы создали массив a и срез s. Мы сравниваем первый элемент массива a с нулем и проверяем, является ли срез s равным nil.
Как мы видим, когда мы объявляем срез, его нулевое значение равно nil. Это происходит потому, что срезы не хранят никаких данных; они только ссылаются на массивы. Срез указывает на базовую структуру массива.
Структура данных среза (Slice)
Срез (slice) представляет собой составной тип данных, также известный как структура (struct). Это составной тип, состоящий из полей разных типов. Внутренняя структура среза состоит из трех элементов: указателя, длины и емкости.
type slice struct {
elem *type
len int
cap int
}
Как упоминалось ранее, структура ссылается на базовый массив. Указатель elem указывает на первый элемент массива, а type - это тип элемента массива, на который делается ссылка.
len и cap представляют соответственно длину и емкость среза. Вы можете использовать функции len() и cap() для получения длины и емкости среза.
На следующем изображении показано, что срез ссылается на базовый массив типа int, имеет длину 8 и емкость 10:
При определении нового среза указатель elem инициализируется нулевым значением (то есть nil). Концепция указателей будет рассмотрена в последующих лабораторных работах. Пока заметим, что указатель указывает на адрес памяти значения. На приведенном выше изображении указатель elem указывает на адрес первого элемента базового массива.
Операции над срезами (Slices): добавление, удаление, изменение и поиск
Усечение массивов или срезов
Поскольку базовой структурой среза является массив, мы можем извлечь заданную длину массива в качестве ссылки для среза. Следующий фрагмент кода демонстрирует это:
package main
import "fmt"
func main() {
// Define an integer array with a length of 10
a := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// Declare an empty slice
var s1 []int
fmt.Println("Slice s1 is empty:", s1 == nil)
// Use array truncation to obtain the slice
s1 = a[1:5]
s2 := a[2:5]
s3 := a[:]
fmt.Println("Slice s1, s2, and s3 are empty:", s1 == nil, s2 == nil, s3 == nil)
fmt.Println("Elements in array a:", a)
fmt.Println("Elements in slice s1:", s1)
fmt.Println("Elements in slice s2:", s2)
fmt.Println("Elements in slice s3:", s3)
}
Вывод будет следующим:
Slice s1 is empty: true
Slice s1, s2, and s3 are empty: false false false
Elements in array a: [0 1 2 3 4 5 6 7 8 9]
Elements in slice s1: [1 2 3 4]
Elements in slice s2: [2 3 4]
Elements in slice s3: [0 1 2 3 4 5 6 7 8 9]
В этой программе мы сначала объявляем и инициализируем массив a, а затем используем усечение массива, чтобы присвоить часть массива пустому срезу s1. Таким образом, мы создаем новый срез.
s1[1:5] представляет создание среза массива. Диапазон среза от индекса 1 массива a до индекса 5, не включая пятый элемент.
Примечание: В языках программирования первый элемент имеет индекс 0, а не 1. Аналогично, второй элемент в массиве имеет индекс 1.
Мы используем оператор :=, чтобы напрямую присвоить усеченный массив срезу s2. То же самое относится и к s3, но диапазон не указан, поэтому он усекает все элементы массива.
Ниже приведено изображение, представляющее усечение. Обратите внимание, что зеленая часть среза представляет ссылку на синий массив. Другими словами, они оба используют один и тот же базовый массив, который является a.
Синтаксис усечения срезов выглядит следующим образом:
[start:end]
И start, и end являются необязательными аргументами. Когда мы хотим получить все элементы массива, мы можем опустить оба аргумента start и end. Это продемонстрировано в s3 := a[:] в предыдущей программе.
Если мы хотим извлечь все элементы после определенного индекса, мы можем опустить параметр end. Например, a1[3:] извлечет все элементы, начиная с индекса 3.
Для извлечения всех элементов до определенного индекса мы можем опустить параметр start. Например, a1[:4] извлечет все элементы от индекса 0 до индекса 4, не включая элемент с индексом 4.
В дополнение к извлечению среза из массива, мы также можем извлечь новый срез из существующего. Операция такая же, как и с массивами. Ниже приведен простой пример:
package main
import "fmt"
func main() {
// Define an integer array with a length of 10
a := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// Create the initial slice s1
var s1 []int
s1 = a[1:7]
fmt.Printf("Slice s1: %d\tLength: %d\tCapacity: %d\n", s1, len(s1), cap(s1))
// Extract a new slice s2 from the initial slice s1
s2 := s1[2:4]
fmt.Printf("Slice s1: %d\tLength: %d\tCapacity: %d\n", s2, len(s2), cap(s2))
}
В этой программе мы получили срез s1 путем усечения массива a. Диапазон s1 от индекса 1 до индекса 7. Используя :=, мы извлекли новый срез s2 из s1. Поскольку усечение s1 является непрерывным, новый срез s2 также будет непрерывным.
Мы замечаем, что емкость среза меняется при усечении. Правила следующие:
Если мы усекаем срез с емкостью c, длина s[i:j] будет равна j - i, а емкость - c - i.
Для s1 базовым массивом является a, и емкость a[1:7] равна 9 (то есть 10 - 1).
Для s2 базовый массив такой же, как у s1. Поскольку усеченная часть имела емкость 9 на предыдущем шаге, емкость s1[2:4] становится равной 7 (то есть 9 - 2).
Изменения значений среза одновременно влияют на значения элементов базового массива
Поскольку срез не хранит данные, а только ссылается на массив, изменение значения среза также одновременно изменит значение базового массива. Давайте продемонстрируем это:
package main
import "fmt"
func main() {
// Define an integer array with a length of 10
a := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := a[2:5]
s2 := a[:]
fmt.Println("Before Modification: ")
fmt.Println("Elements in array a: ", a)
fmt.Println("Elements in array a: ", s1)
fmt.Println("Elements in array a: ", s2)
// Modify the value at index 2 of the slice s1 to 23
s1[2] = 23
fmt.Println("After Modification: ")
fmt.Println("Elements in array a: ", a)
fmt.Println("Elements in array a: ", s1)
fmt.Println("Elements in array a: ", s2)
}
Вывод будет следующим:
Before Modification:
Elements in array a: [0 1 2 3 4 5 6 7 8 9]
Elements in array a: [2 3 4]
Elements in array a: [0 1 2 3 4 5 6 7 8 9]
After Modification:
Elements in array a: [0 1 23 3 4 5 6 7 8 9]
Elements in array a: [2 3 23]
Elements in array a: [0 1 23 3 4 5 6 7 8 9]
В этой программе и срез s1, и срез s2 ссылаются на массив a. Когда срез s1 изменяет значение с индексом 2 на 23, значения массива a и среза s2 также обновляются.
Мы видим, что значение с индексом 2 в массиве изменено на 23, что приводит к изменению значения с индексом 4 в срезе s2.
Это может привести к трудно отлаживаемым ошибкам в разработке программы. Поэтому в повседневном программировании мы должны максимально избегать ситуации, когда несколько срезов ссылаются на один и тот же базовый массив.
Добавление элементов в срез
В этом разделе мы рассмотрим функцию append, которая используется для добавления элементов в срез. Синтаксис выглядит следующим образом:
func append(slice []Type, elems...Type) []Type
Первый аргумент - это срез slice, а остальные аргументы - элементы, которые нужно добавить в срез. []Type в конце указывает, что функция append вернет новый срез с тем же типом данных, что и slice.
elems после ... означает, что это переменный параметр, то есть можно ввести один или несколько параметров.
В этой программе мы сначала создаем срез s1 путем усечения массива a. Диапазон s1 от индекса 1 до индекса 7. Мы используем оператор :=, чтобы присвоить возвращаемое значение функции append срезу s1. Когда мы добавляем элемент с помощью append, емкость среза может измениться. Емкость s1 будет удваиваться, если количество элементов превысит емкость.
Правила расширения срезов следующие:
Если базовый массив среза может вместить новые элементы, емкость среза не изменится.
Если базовый массив среза не может вместить новые элементы, Go создаст больший массив, скопирует значения исходного среза в новый массив, а затем добавит добавляемое значение в новый массив.
Примечание: Расширение среза не всегда происходит путем удвоения; это зависит от размера элементов, количества расширяемых элементов, аппаратного обеспечения компьютера и других факторов. Для получения более подробной информации обратитесь к разделу о срезах для продвинутых пользователей.
Удаление элементов из среза
Go не предоставляет ключевых слов или функций для удаления элементов из среза, но мы можем использовать усечение массива, чтобы достичь той же функциональности или даже более мощной.
Следующий код удаляет элемент с индексом 5 в срезе s и присваивает результат новому срезу s1:
package main
import "fmt"
func main() {
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// Print the slice `s` before deleting
fmt.Println(s)
s1 := append(s[:5], s[6:]...)
// Print the slices `s` and `s1` after deleting the element at index 5
fmt.Printf("%d\n%d\n", s, s1)
}
Ключ к удалению определенного индекса заключается в операторе append. Он добавляет элементы после индекса 6 к элементам до индекса 5.
Эта операция эквивалентна предварительному перезаписыванию. Например, элемент 5 перезаписывается элементом 6, элемент 6 - элементом 7 и так далее, пока не дойдем до элемента 9, который добавляется в новый срез. Поэтому значение базового массива в этот момент равно [0 1 2 3 4 6 7 8 9 9].
В этот момент, если мы проверим длину и емкость s1, мы увидим, что длина равна 9, а емкость - 10. Это происходит потому, что s1 является ссылкой на усечение s. Они используют один и тот же базовый массив.
package main
import "fmt"
func main() {
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Println(s)
s1 := append(s[:5], s[6:]...)
// Check the length and capacity of s1
fmt.Println(len(s1), cap(s1))
fmt.Printf("\n%d\n%d\n\n", s, s1)
// Modify the slice s
s[3] = 22
fmt.Printf("%d\n%d\n", s, s1)
}
В дополнение к удалению элементов в определенных позициях, мы также можем удалить определенный диапазон элементов из среза с помощью усечения. Операция такая же, как при удалении элемента с определенным индексом. Давайте выполним небольшое упражнение.
Создайте файл slice1.go. Создайте срез a и инициализируйте его следующим образом. Затем используйте усечение, чтобы создать другой срез s, который не включает элементы больше 3 или меньше 7. В конце распечатайте новый срез.
a := []int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
Вывод:
[9 8 7 3 2 1 0]
Подсказка: Обратите внимание на начальный индекс среза (помните, что индекс начинается с 0).
Требования: Файл slice1.go должен быть расположен в директории ~/project.
Срез имеет два атрибута: len и cap. len представляет количество элементов, находящихся в срезе в данный момент, в то время как cap представляет максимальное количество элементов, которое может вместить срез.
Что происходит, когда количество элементов, добавленных в срез, превышает его емкость? Давайте узнаем вместе:
Для запуска программы выполните следующую команду:
go run slice.go
Вывод будет следующим:
Before Append in s1: [0 0 0] Length: 3 Capacity: 3
Before Append in s2: [0 0 0] Length: 3 Capacity: 5
After Append in s1: [0 0 0 12] Length: 4 Capacity: 6
After Append in s2: [0 0 0 22] Length: 4 Capacity: 5
Как мы видим в этой программе, когда мы добавляем элементы в срез и количество элементов превышает его исходную емкость, емкость среза автоматически увеличивается.
Правила расширения среза следующие:
Если базовый массив среза может вместить новые элементы, емкость среза не изменится.
Если базовый массив среза не может вместить новые элементы, Go создаст больший массив, скопирует значения из исходного среза в новый массив, а затем добавит добавляемые значения в новый массив.
Примечание: Удвоение емкости среза не всегда происходит; это зависит от размера элементов, количества элементов и аппаратного обеспечения компьютера. Для получения более подробной информации обратитесь к разделу о срезах для продвинутых пользователей.
Копирование срезов (Slices)
Мы можем использовать функцию copy для копирования одного среза в другой. Синтаксис выглядит следующим образом:
func copy(dst, src []Type) int
dst - это целевой срез, src - исходный срез, а конечный int указывает количество скопированных элементов, которое равно меньшему из значений len(dst) и len(src).
Для запуска программы выполните следующую команду:
go run slice.go
Вывод будет следующим:
2 [0 1 2 3] [0 1]
2 [8 9 2 3] [8 9]
В этой программе мы скопировали значения среза s1 в срез s2 и значения среза s4 в срез s3. Функция copy возвращает количество скопированных элементов.
Мы замечаем, что значения s1 и s2 совпадают, также как и значения s3 и s4. Первая функция copy копирует s1[0, 1, 2, 3] в s2[8, 9]. Поскольку минимальная длина s1 и s2 равна 2, она копирует 2 значения. Целевой срез s2 изменяется на [0, 1].
Вторая функция copy копирует s4[8, 9] в s3[0, 1, 2, 3]. Поскольку минимальная длина s3 и s4 равна 2, она копирует 2 значения. Таким образом, s3 изменяется на [8, 9, 2, 3].
Перебор элементов срезов (Slices)
Перебор элементов среза аналогичен перебору элементов массива. Все методы перебора массивов также могут быть использованы для срезов.
Упражнение
В этом упражнении мы проверим и закрепим наше понимание перебора элементов срезов и массивов.
Создайте файл slice2.go. Объявите массив a1 и срез s1 и инициализируйте их следующим образом. Затем переберите элементы массива a1 и среза s1 и выведите их индексы и значения.
Element a1 at index 0 is 1
Element a1 at index 1 is 2
Element a1 at index 2 is 3
Element a1 at index 3 is 9
Element a1 at index 4 is 7
Element s1 at index 0 is 1
Element s1 at index 1 is 8
Element s1 at index 2 is 12
Element s1 at index 3 is 1
Element s1 at index 4 is 3
Требования: Файл slice2.go должен быть расположен в директории ~/project.
Подсказка: Вы можете использовать формат range или формат с индексами для перебора элементов.
В этом разделе мы изучили срезы (slices) и их применение. По сравнению с массивами, срезы более гибкие и универсальные. При работе с несколькими срезами необходимо быть осторожными, чтобы избежать непредвиденных операций над срезами.