Полное руководство по массивам и срезам в Golang

Original author: Soham Kamani
  • Translation
Перевод статьи подготовлен специально для студентов курса «Разработчик 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}


Если просто смотреть на код, то кажется, что срезы и массивы достаточно похожи, но основное их отличие лежит в реализации и условиях использования.

Представление в памяти


Срез аллоцируется иначе, чем массив, и по сути является модифицированным указателем. Каждый срез содержит в себе три блока информации:

  1. Указатель на последовательность данных.
  2. Длину (length), которая определяет количество элементов, которые сейчас содержатся в срезе.
  3. Объем (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 берет срез из целых чисел и сортирует их по возрастанию значений. Срезы здесь использовать предпочтительнее по двум причинам:

  1. Количество целых чисел не указано (количество целых чисел для сортировки может быть любым);
  2. Числа нужно отсортировать по возрастанию. Использование массива обеспечит передачу всей коллекции целых чисел в качестве значения, поэтому функция будет сортировать свою собственную копию, а не переданную ей коллекцию.


Заключение


Теперь, когда мы рассмотрели ключевые различия между массивами и срезами, а также их варианты использования, я хочу дать несколько советов, чтобы вам было проще решить, какую конструкцию следует использовать:

  1. Если сущность описывается набором непустых элементов фиксированной длины – используйте массивы.
  2. При описании коллекции, к которой вы хотите добавить или из которой удалить элементы – используйте срезы.
  3. Если коллекция может содержать любое количество элементов, используйте срезы.
  4. Будете ли вы каким-то образом изменять коллекцию? Если да, то следует использовать срезы.


Как видите, срезы охватывают большинство сценариев для создания приложений на Go. Тем не менее, массивы имеют право на существование, и более того, невероятно полезны, особенно когда появляется подходящий вариант использования.
OTUS. Онлайн-образование
Цифровые навыки от ведущих экспертов

Comments 7

    –1
    Картинка к «Передача (слайса) по ссылке» может запутать, так как после присвоения оба слайса становятся полностью независимыми play.golang.org/p/7_SqvjGKPDE
      +1
      Не становятся.
      В вашем примере «независимыми» их делает append — который создаёт новый слайс на основе переданного и добавляемых элементов: play.golang.org/p/xI1nlgjKH4t
        0
        Да, так и есть.
          +2

          append не всегда создаёт новый слайс

        +1
        Мне кажется срезы не совсем не правильно описано, что приведет в заблуждение новичков. Почитайте книгу Алана Долована «Язык программирования Go». Срез всегда ссылается «под капотом» на массив, по этому он всегда будет «ссылкой»(не указателем в go). Если вы расширяете срез и он больше чем массив «под капотом», то «под капотом» создастся новый массив требуемой длинны и срез теперь будет указывать на него. Но если срез будет уменьшаться, то массив не будет пересоздан, можно узнать так как вы показывали len и cap. На этом можно ловить баги.

        Может я ошибаюсь или неправильно понял статью. Вот пример play.golang.org/p/ENpGACeRrFn

        И можно сразу указывать длину массива, при создании среза, что бы потом не пересоздавать массивы — make([]int, 0, 10)
          +1

          В Go нету ссылок.
          Слайс в Go — это структура, где одно из полей это указатель на массив:


          type slice struct {
             array unsafe.Pointer
             len   int
             cap   int
          }

          https://golang.org/src/runtime/slice.go

          0
          Есть у меня предположение, что в последнем квадратике адрес должен быть 16-ричным:
          картинка
          image

          0x414020 0x414024 0x414028 0x41402C
          Знаю, что перевод…

          upd: заметил, что в некоторых других картинках тоже есть подобные ошибки.

          Only users with full accounts can post comments. Log in, please.