Массивы и срезы

Массивы

Массивы — это упорядоченный набор элементов определенного типа фиксированной длины. Объявление массива осуществляется следующим образом:

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.