[Перевод] Полное руководство по массивам и срезам в Golang
Перевод статьи подготовлен специально для студентов курса «Разработчик Golang», занятия по которому начинаются уже сегодня!
Поначалу легко воспринимать массивы и срезы как одно и то же, но с разными названиями: и то и другое является структурой данных для представления коллекций. Однако на самом деле они сильно отличаются друг от друга.
В этой статье мы рассмотрим их различия и реализации в Go.
Мы обратимся к примерам, чтобы вы могли принимать более взвешенное решение о том, где их применять.
Массивы
Массив — это коллекция фиксированного размера. Акцент здесь ставится именно на фиксированный размер, поскольку, как только вы зададите длину массива, позже вы уже не сможете ее изменить.
Давайте рассмотрим пример. Мы создадим массив из четырех целых значений:
arr := [4]int{3, 2, 5, 4}
Длина и тип
В примере выше переменная arr
определена как массив типа [4]int
, это означает, что массив состоит из четырех элементов. Важно обратить внимание на то, что размер 4
включен в определение типа.
Из этого исходит, что на самом деле массивы разной длины — это массивы разных типов. Вы не сможете приравнять друг к другу массивы разной длины и не сможете присвоить значение одного массива другому в таком случае:
longerArr := [5]int{5, 7, 1, 2, 0}
longerArr = arr
// This gives a compilation error
longerArr == arr
// This gives a compilation error
Я обнаружил, что о массивах легко рассуждать с точки зрения структур. Если бы вы попытались создать структуру подобную массиву, у вас скорее всего получилось бы следующее:
// Struct equivalent for an array of length 4
type int4 struct {
e0 int
e1 int
e2 int
e3 int
}
// Struct equivalent for an array of length 5
type int5 struct {
e0 int
e1 int
e2 int
e3 int
e5 int
}
arr := int4{3, 2, 5, 4}
longerArr := int5{5, 7, 1, 2, 0}
На самом деле так делать не рекомендуется, однако это хороший способ получить представление о том, почему массивы разной длины являются массивами разного типа.
Представление в памяти
Массив хранится в виде последовательности из n
блоков определенного типа:
Эта память распределяется в момент, когда вы инициализируете переменную типа массив.
Передача по ссылке
В Go нет такой вещи, как передача по ссылке, вместо этого все передается по значению. Если присвоить значение массива другой переменной, то присваиваемое значение просто будет скопировано.
Если вы хотите передать лишь «ссылку» на массив, используйте указатели:
При распределении памяти и в функции массив на самом деле является простым типом данных и работает во многом аналогично структурам.
Срезы
Срезы можно рассматривать как расширенную реализацию массивов.
Срезы были реализованы в Go, чтобы покрыть некоторые крайне распространенные варианты использования, с которыми разработчики сталкиваются при работе с коллекциями, например, динамическое изменение размера коллекций.
Объявление среза очень похоже на объявление массива, за исключением того, что опускается спецификатор длины:
slice := []int{4, 5, 3}
Если просто смотреть на код, то кажется, что срезы и массивы достаточно похожи, но основное их отличие лежит в реализации и условиях использования.
Представление в памяти
Срез аллоцируется иначе, чем массив, и по сути является модифицированным указателем. Каждый срез содержит в себе три блока информации:
- Указатель на последовательность данных.
- Длину (length), которая определяет количество элементов, которые сейчас содержатся в срезе.
- Объем (capacity), который определяет общее количество предоставленных ячеек памяти.
Из этого следует, что срезы разной длины можно присваивать друг другу. Они имеют один и тот же тип, а указатель, длина и объем могут меняться:
slice1 := []int{6, 1, 2}
slice2 := []int{9, 3}
// slices of any length can be assigned to other slice types
slice1 = slice2
Срез, в отличии от массива, не выделяет память во время инициализации. Фактически, срезы инициализируется с нулевым (nil
) значением.
Передача по ссылке
Когда вы присваиваете срез другой переменной, вы все еще передаете значение. Здесь значение обращается только к указателю, длине и объему, а не к памяти, занимаемой самими элементами.
Добавление новых элементов
Чтобы добавить новые элементы к срезу, необходимо использовать функцию append
.
nums := []int{8, 0}
nums = append(nums, 8)
Под капотом это будет выглядеть, как присвоение значения, указанного для нового элемента, и после — возвращение нового среза. Длина нового среза будет на единицу больше.
Если при добавлении элемента длина увеличивается на единицу и тем самым превышает заявленный объем, необходимо предоставить новый объем (в этом случае текущий объем обычно удваивается).
Именно поэтому чаще всего рекомендуется создавать срез с длиной и объемом, указанными заранее (особенно, если вы четко имеете представление какого размера срез вам нужен):
arr := make([]int, 0, 5)
// This creates a slice with length 0 and capacity 5
Что использовать: массивы или срезы?
Массивы и срезы — это совершенно разные вещи, и, следовательно, их варианты использования также разнятся.
Давайте рассмотрим несколько примеров с открытыми исходниками и стандартную библиотеку Go, чтобы понять, что и в каких случаях использовать.
Кейс 1: UUID
UUID — это 128-битные фрагменты данных, их часто используют для маркировки объекта или сущности. Обычно они представлены в виде шестнадцатеричных значений, разделенных тире:
e39bdaf4-710d-42ea-a29b-58c368b0c53c
В библиотеке Google UUID, UUID представлен как массив из 16 байт:
type UUID [16]byte
Это имеет смысл, поскольку мы знаем, что UUID состоит из 128 бит (16 байт). Мы не собираемся добавлять или удалять какие-либо байты из UUID, и поэтому использование массива для его представления будет.
Кейс 2: Сортировка целых значений
В этом примере мы будем использовать функцию sort.Ints
из sort standard library:
s := []int{5, 2, 6, 3, 1, 4} // unsorted
sort.Ints(s)
fmt.Println(s)
// [1 2 3 4 5 6]
Функция sort.Ints
берет срез из целых чисел и сортирует их по возрастанию значений. Срезы здесь использовать предпочтительнее по двум причинам:
- Количество целых чисел не указано (количество целых чисел для сортировки может быть любым);
- Числа нужно отсортировать по возрастанию. Использование массива обеспечит передачу всей коллекции целых чисел в качестве значения, поэтому функция будет сортировать свою собственную копию, а не переданную ей коллекцию.
Заключение
Теперь, когда мы рассмотрели ключевые различия между массивами и срезами, а также их варианты использования, я хочу дать несколько советов, чтобы вам было проще решить, какую конструкцию следует использовать:
- Если сущность описывается набором непустых элементов фиксированной длины — используйте массивы.
- При описании коллекции, к которой вы хотите добавить или из которой удалить элементы — используйте срезы.
- Если коллекция может содержать любое количество элементов, используйте срезы.
- Будете ли вы каким-то образом изменять коллекцию? Если да, то следует использовать срезы.
Как видите, срезы охватывают большинство сценариев для создания приложений на Go. Тем не менее, массивы имеют право на существование, и более того, невероятно полезны, особенно когда появляется подходящий вариант использования.