Массивы и срезы
Массивы
Массивы — это упорядоченный набор элементов определенного типа фиксированной длины. Объявление массива осуществляется следующим образом:
var array [3]int
fmt.Println(array) // [0 0 0]
При объявлении массива в квадратных скобках указывается его длина (length), которая совместно с типом элементов, определяет тип самого массива. Набор элементов в массиве строго типизированный — это означает, что в одном массиве не могут содержаться разные элементы типов (т.е. в массив int'ов нельзя добавить элемент string и наоборот).
Как мы увидели в приведенном примере, объявленный массив был при объявлении инициализирован нулевыми значениями (0 для int). В случае объявления масссива типа string данный массив будет вовсе пустой:
var arrOfStr [1]string
fmt.Println(arrOfStr) // []
Конечно же можно и во время объявления массива сразу инициализировать его элементы:
var array [3]int = [3]int{1, 2, 3}
Обратите внимание, что значения передаются в фигурных скобках через запятую. При этом значений не может быть больше длины массива! Получается, что в данном случае длина массива равна 3,следовательно, нельзя в фигурных скобках определить больше пяти элементов. Однако, можно определить меньшее количество элементов:
var array [3]int = [3]int{1}
fmt.Println(array) // [1 0 0]
Таким образом, элементы, для которых не указано значение, будут иметь значение по умолчанию.
Также можно скоращенно определять переменные массива:
elements := [4]int{1, 2, 3, 4}
Если в квадратных скобках вместо длины указано троеточие, то длина массива определяется, исходя из количества переданных ему элементов:
elements1 := [...]int{1, 2, 3, 4}
elements2 := [...]int{1, 2, 3, 4, 5, 6}
fmt.Println(len(elements1)) // 4
fmt.Println(len(elements2)) // 6
Примечание: len() — это функция, которая возвращает длину входного параметра (будто массив, срез или даже строка). Подробно о функциях будет сказано в Главе 2.
При этом следует учитывать тот факт, что длина массива является частью его типа. Так, например, elements1
и elements2
представляют разные типы данных, хотя они и хранят данные одного типа.
Индексы
Для обращения к элементам массива применяются индексы - номера элементов, т.е. по индексу можно получить значение элемента, либо изменить его. В Go (как и почти во всех других языках программирования) нумерация элементов массива начинается с нуля, то есть первый элемент будет иметь порядковый нидекс 0. Индексы указываются в квадратных скобка:
package main
import "fmt"
func main() {
var numbersOfInts [4]int = [4]int{1,2,3,4}
fmt.Println(numbersOfInts[0]) // 1
fmt.Println(numbersOfInts[3]) // 4
numbersOfInts[0] = 100
fmt.Println(numbersOfInts[0]) // 100
fmt.Println(numbersOfInts) // [100 2 3 4]
}
Срезы
Срезы — это последовательность элементов одного типа переменной длины (ключевое отличие от массива). По сравнению с массивами, в срезах длина не фиксирована и в ходе выполнения программы она может меняться, то есть можно добавлять или удалять элементы среза.
Каждый срез содержит в себе три компонента информации — указателя,длины и емкости:
- указатель на первый элемент массива, доступный через срез;
- длина (length), которая определяет количество элементов, которые содержатся в срезе данный момент. Длина среза может быть определена при помоци встроенной функции len();
- емкость (capacity), которая определяет количество выделенных ячеек для среза.
Примечание от автора: тема с аллоцированием памяти среза и массива очень сложная для новичков, так как данная тема непосредственно связана с указателями (которые будут рассмотрены во второй главе) и работой памяти компьютера в целом (т.е. что такое память, классификации памяти, адрес, виды адресаций и т.п.). Поэтому для полного понимания и усвоения данной темы рекомендую сначала ознакомиться с данным видео, затем усвоить тему с указателями и только потом можно будет приступать к изучению аллоцирования памяти массивов и срезов.
Создание среза
Срез определяется также, как и массив, за исключением того факта, что у среза не указывается длина:
var numbers []int
Можно и сразу иницилизировать значения среза:
var numbers = []int{1, 2, 3, 4}
Или таким образом:
numbers := []int{1, 2, 3, 4}
К элементам среза обращение происходит также, как и к элементам массива, т.е. по индексу:
var numbers = []int{1, 2, 3, 4}
fmt.Println(numbers[2]) // 3
Добавление в срез
Для добавления в срез применяется встроенная функция append(), которая имеет следующий вид:
append(slice, value)
Первый параметр функции apend() - срез, в который надо добавить, а второй параметр - значение, которое нужно добавить. Результатом функции является увеличенный срез:
package main
import "fmt"
func main() {
var numbers = []int{1, 2, 3, 4}
numbers = append(numbers, 5)
fmt.Println(numbers) // [1 2 3 4 5]
}
Можно добавлять сразу несколько элементов в срез:
package main
import "fmt"
func main() {
var numbers = []int{1, 2, 3, 4}
numbers = append(numbers, 5, 6, 7)
fmt.Println(numbers) // [1 2 3 4 5 6 7]
}
Также с помощью операторы ... можно объединять два среза в один или элементы одного среза добавить в другой срез:
package main
import "fmt"
func main() {
var numbers1 = []int{1, 2, 3, 4}
var numbers2 = []int{5, 6, 7}
numbers1 = append(numbers1, numbers2...)
fmt.Println(numbers1) // [1 2 3 4 5 6 7]
}
Оператор среза
Оператор среза slice[i:j] создает из последовательности slice новый срез, который содержит элементы последовательности slice с i по j-1 (j не включительно!). При этом должно соблюдаться условие 0 <= i <= j <= cap(s). В качестве исходной последовательности, из которой берутся элементы, может использоваться массив, указатель на массив или другой срез. В итоге в полученном срезе будет j-i элементов:
package main
import "fmt"
func main() {
var numbers1 = [7]int{1, 2, 3, 4, 5, 6, 7} // это массив
var numbers2 = numbers1[1:5] // а это слайс, который ссылается на массив.
fmt.Println(numbers2) // [2 3 4 5] // элемент 6 не включен в данный слайс, т.к. последовательность элементов слайса идут с i по j-1
}
Если значение i не указано, то применяется по умолчанию значение 0 (т.е. стартовый элемент исходного массива/среза). Если значение j не указано, то вместо него используется длина исходной последовательности массива/среза:
package main
import "fmt"
func main() {
var numbers1 = [7]int{1, 2, 3, 4, 5, 6, 7}
var numbers2 = numbers1[:3]
var numbers3 = numbers1[3:]
fmt.Println(numbers2) // [1 2 3]
fmt.Println(numbers3) // [4 5 6 7]
}
Удаление элемента в срезе
К сожалению, в Go отсутствует встроенная функция для удаления элемента из среза, но мы можем воспользоваться встроенной функцией append() для того, чтобы создать новый срез, включающий в себя срез элементов до игнорируемого элемента, а также все элементы после игнорируемого. Например, мы хотим убрать значение 4
в исходном массиве:
package main
import "fmt"
func main() {
var numbers1 = [7]int{1, 2, 3, 4, 5, 6, 7}
numbers2 := append(numbers1[:3], numbers1[4:]...)
fmt.Println(numbers2) // [1 2 3 5 6 7]
}
Функция make()
Существует встроенная функция make(), которая может содержать до 3-х параметров (тип, длина и емкость):
make([]T, len(), cap())
Чтобы разобраться с этой функцией нам нужно посмотреть, что происходит при создании среза. Количество элементов, что видны в срезе определяют ее длину (len()):
package main
import "fmt"
func main() {
var numbers1 = [7]int{1, 2, 3, 4, 5, 6, 7}
var numbers2 = numbers1[:3]
fmt.Println(numbers2) // [1 2 3]
fmt.Println(len(numbers2)) // 3
}
Как мы уже знаем, у слайса есть базовый массив или срез, на который он может ссылаться. В данном примере срез numbers2
ссылается на массив numbers1
. Если с параметрами тип ([]T) и длина (len()) уже понятно, то параметр емкость (cap()) показывает возможность добавления элементов в срез numbers2
без необходимости выделения нового массива, на который будет ссылаться numbers2
, если превысить показатель параметра емкости для среза numbers2
:
package main
import "fmt"
func main() {
var numbers1 = [7]int{1, 2, 3, 4, 5, 6, 7}
var numbers2 = numbers1[:3]
fmt.Println(numbers2) // [1 2 3]
fmt.Println(len(numbers2)) // 3
fmt.Println(cap(numbers2)) // 7
}
Т.е., исходя из примера, можно сказать что емкость для среза numbers2
это длина массива numbers1
, на который ссылается numbers2
. Если превысить данный показатель, то создастся новый массив с удвоенной длинной (т.е. 14 вместо 7), на который будет ссылаться наш срез numbers2
:
package main
import "fmt"
func main() {
var numbers1 = [7]int{1, 2, 3, 4, 5, 6, 7}
var numbers2 = numbers1[:3]
fmt.Println(numbers2) // [1 2 3]
fmt.Println(len(numbers2)) // 3
fmt.Println(cap(numbers2)) // 7
numbers2 = append(numbers2, 4, 5, 6, 7, 8)
fmt.Println(numbers2) // [1 2 3 4 5 6 7 8]
fmt.Println(len(numbers2)) // 8
fmt.Println(cap(numbers2)) // 14, так как мы превысили емкость, то "внутри" программы создался новый массив (мы его не видим) с длиной 14, на который ссылается срез numbers2
}
Однако так делать крайне не рекомендуется, т.к. создание выделяется дополнительная память для программы, которая, в свою очередб негативно сказывается на производительность программы в целом. Поэтому с помощью функции make() всегда нужно четко определять емкость того или иного среза.
Таким образом, ключевое преимущество создания среза через make() в том, что предварительное выделение через make() может установить начальную вместимость, тем самым можно избежать дополнительных перемещений и копий для увеличения базового массива.
Функция copy()
Встроенная функция copy копирует элементы в целевой срез dst из исходного среза src:
copy(dst, src []Type)
Данная функция возвращает число скопированных элементов:
package main
import "fmt"
func main() {
var numbers1 = []int{1, 3, 4}
numbers2 := make([]int, 3)
countOfCopyEl := copy(numbers2, numbers1)
fmt.Println(numbers1) // [1 3 4]
fmt.Println(numbers2) // [1 3 4]
fmt.Println(countOfCopEl) // 3 - количесвто скопированных элементов
}
В данном примере мы явно указали длину создаваемого среза numbers2, т.к. при длине 1, был бы скопирован 1 элемент из среза numbers1.