Что возвращает функция cap применительно к указателю на массив golang
Golang
Блог о языке программирования Go
среда, 12 февраля 2020 г.
Срезы в Golang: внутреннее устройство и использование
Тип среза (slice) в Go предоставляет удобный и эффективный способ работы с последовательностями типизированных данных. Срезы аналогичны массивам на других языках, но имеют некоторые необычные свойства. В этом посте мы рассмотрим, что такое срезы и как они используются.
Массивы
Определение типа массива определяет длину и тип элемента. Например, тип [4]int представляет массив из четырех целых чисел. Размер массива фиксирован; его длина является частью его типа ([4]int и [5]int являются различными несовместимыми типами). Массивы можно индексировать обычным способом, поэтому выражение s[n] обращается к n-му элементу, начиная с нуля.
Литерал массива может быть указан так:
Или вы можете сделать так, чтобы компилятор подсчитал для вас элементы массива:
Срезы
Массивы имеют свое место, но они немного негибкие, поэтому вы не видите их слишком часто в коде Go. Срезы, однако, есть везде. Они основаны на массивах, чтобы обеспечить большую мощность и удобство.
Литерал среза объявляется так же, как литерал массива, за исключением того, что вы пропускаете количество элементов:
Срез может быть создан с помощью встроенной функции под названием make, которая имеет сигнатуру,
где T обозначает тип элемента среза, который будет создан. Функция make принимает тип, длину и дополнительную емкость. При вызове make выделяет массив и возвращает срез, который ссылается на этот массив.
Если аргумент емкости (cap) опущен, по умолчанию используется указанная длина. Вот более краткая версия того же кода:
Длину и емкость среза можно проверить с помощью встроенных функций len и cap.
В следующих двух разделах обсуждается связь между длиной и емкостью.
Нулевое значение среза равно nil. Функции len и cap возвращают 0 для нулевого среза.
Срез также может быть сформирован путем «нарезки» существующего среза или массива. Разрезание выполняется путем указания полуоткрытого диапазона с двумя индексами, разделенными двоеточием. Например, выражение b [1:4] создает срез, включающий элементы с 1 по 3 из b (индексы полученного среза будут от 0 до 2).
Начальный и конечный индексы выражения среза являются необязательными; по умолчанию они равны нулю и длине среза соответственно:
Это также синтаксис для создания среза по массиву:
Внутренности среза
Наша переменная s, созданная ранее make([]byte, 5), имеет следующую структуру:
Когда мы срезаем s, наблюдаем за изменениями в структуре данных среза и их отношением к базовому массиву:
Нарезка не копирует данные среза. Он создает новое значение среза, которое указывает на исходный массив. Это делает операции среза такими же эффективными, как манипулирование индексами массива. Следовательно, изменение элементов (не самого среза) повторного среза изменяет элементы исходного среза:
Ранее мы нарезали s до длины, меньшей его емкости. Мы можем увеличить s до его емкости, нарезав его снова:
Срез не может быть выращен за пределами его емкости. Попытка сделать это вызовет панику во время выполнения (runtime panic), так же как и при индексировании вне границ среза или массива. Точно так же срезы нельзя повторно разрезать ниже нуля для доступа к более ранним элементам в массиве.
Растущие срезы (функции copy и append)
Цикл этой общей операции упрощается благодаря встроенной функции copy. Как следует из названия, copy копирует данные из исходного среза в целевой срез. Возвращает количество скопированных элементов.
Функция copy поддерживает копирование между срезами разной длины (она будет копировать только до меньшего количества элементов). Кроме того, copy может обрабатывать срезы источника и назначения, которые совместно используют один и тот же базовый массив, правильно обрабатывая перекрывающиеся срезы.
Используя copy, мы можем упростить приведенный выше фрагмент кода:
Обычной операцией является добавление данных в конец среза. Эта функция добавляет байтовые элементы к срезу байтов, при необходимости увеличивая его, и возвращает обновленное значение среза:
Можно использовать AppendByte следующим образом:
Такие функции, как AppendByte, полезны, потому что они предлагают полный контроль над тем, как растет срез. В зависимости от характеристик программы, может быть желательно распределить ее на более мелкие или большие куски или ограничить размер перераспределения.
Но большинству программ не требуется полный контроль, поэтому Go предоставляет встроенную функцию append, которая подходит для большинства целей; ее сигнатура
Функция append добавляет элементы x в конец среза s и увеличивает его, если требуется большая емкость.
Поскольку нулевое значение среза (nil) действует как срез нулевой длины, вы можете объявить переменную среза и затем добавить ее в цикл:
Трюк со срезом
Как упоминалось ранее, повторная нарезка среза не делает копию базового массива. Полный массив будет храниться в памяти до тех пор, пока на него больше не будут ссылаться. Иногда это может привести к тому, что программа сохранит все данные в памяти, когда требуется только небольшой срез.
Например, эта функция FindDigits загружает файл в память и ищет в нем первую группу последовательных числовых цифр, возвращая их как новый фрагмент.
Этот код ведет себя как объявлено, но возвращенный []byte указывает на массив, содержащий весь файл. Поскольку срез ссылается на исходный массив, пока срез хранится вокруг сборщика мусора, он не может освободить массив; несколько полезных байтов файла сохраняют все содержимое в памяти.
Чтобы решить эту проблему, можно скопировать интересные данные в новый срез перед возвратом:
Указатели в Golang
Указатель является переменной, что указывает на адрес другой переменной. В программировании указатели являются формой косвенной адресации, что может быть довольно мощным инструментом.
После изучения данного урока вы сможете:
Рекомендуем вам супер TELEGRAM канал по Golang где собраны все материалы для качественного изучения языка. Удивите всех своими знаниями на собеседовании! 😎
Мы публикуем в паблике ВК и Telegram качественные обучающие материалы для быстрого изучения Go. Подпишитесь на нас в ВК и в Telegram. Поддержите сообщество Go программистов.
Практически у каждого городского здания есть небольшая табличка, где указано название улицы и номер дома. Такая система помогает ориентироваться в местности и помогает избежать путаницы. На двери закрывшихся магазинов нередко приклеивают объявления с пояснениями вроде «Извините, мы переехали!» и новым адресом. Указатели в программировании напоминают записки подобного рода, что указывают на другой адрес.
Содержание статьи
Все проблемы в программировании можно решить через очередной уровень косвенной адресации…
Указатели очень полезны, однако за ними сложилась не очень приятная репутация. Языки прошлого, С в частности, делают акцент на безопасности. Сбои и уязвимости в защите зачастую связаны с неправильным использованием указателей. Вследствие этого, стал заметен рост популярности тех языков, что не сильно полагаются на указатели.
В Go также есть указатели, и они учитывают вопросы безопасности. Go не заботят проблемы висячих указателей. Их можно сравнить со случаем, когда вы направляетесь в любимый магазин, однако, прибыв на место, внезапно видите парковку больницы.
Если вы раньше работали с указателями, не переживайте. Сейчас все будет намного лучше. Если вы сталкиваетесь с указателями впервые, расслабьтесь. Go станет отличной отправной точкой для изучения указателей.
Как и вывеска на дверях магазина, что указывает на новый адрес, указатели направляют компьютер к месту, где нужно искать запрашиваемое значение. Подумайте о других жизненных ситуациях, что похожи на работу указателей.
Амперсанд (&) и звездочка астериск (*) в Golang
Указатели в Go адаптируют хорошо-установившийся синтаксис, используемый С. Стоит обратить внимание на два символа — амперсанд ( & ) и звездочка астериск ( * ). Однако у звездочки два назначения, о которых вы узнаете далее.
Оператор адреса представлен амперсандом. Он определяет адрес переменной в памяти. Значения переменных хранятся в памяти RAM, а место хранения переменной называют адресом памяти. Следующий код выводит адрес памяти шестнадцатеричного числа. Обратите внимание, что адрес на вашем компьютере будет отличаться.
Как не наступать на грабли в Go
Этот пост является версией моей же англоязычной статьи «How to avoid gotchas in Go», но слово gotcha не переводится на русский, поэтому я буду использовать это слово как без перевода, так и немного непрямой вариант — «наступать на грабли».
Gotcha — корректная конструкция системы, программы или языка программирования, которая работает, как описано, но, при этом, контринтуитивна и является причиной ошибок, поскольку её легко использовать неверно.
В языке Go есть несколько таких gotchas и есть немало хороших статей, которые их подробно описывают и разъясняют. Я считаю, что эти статьи очень важны, особенно для новичков в Go, поскольку регулярно вижу людей, попадающихся на те же грабли.
Но один вопрос меня мучал долгое время — почему я сам никогда не делал этих ошибок? Серьезно, самые популярные из них, вроде путаницы с nil-интерфейсом или непонятного результата при append()-е слайса — в моей практике никогда не были проблемой. Каким-то образом мне повезло обойти эти подводные камни с первых дней своей работы с Go. Что же мне помогло?
И ответ оказался довольно прост. Я просто очень вовремя прочёл несколько хороших статей о внутреннем устройстве структур данных в Go и прочих деталях реализации. И этого, вполне поверхностного на самом деле, знания было достаточно, чтобы выработать некоторую интуицию и избегать этих подводных камней.
Давайте вернёмся к определению, «gotcha… это корректная конструкция… которая контринтуитивна. «. В этом вся соль. У нас есть, на самом деле, два варианта:
Первый вариант, который будет по душе многим хабрачитателям, конечно же не вариант. В Go есть обещание обратной совместимости — язык уже меняться не будет, и это прекрасно — программы написанные в 2012-м компилируются сегодня последней версией Go без единого ворнинга. Кстати, в Go нет ворнингов 🙂
Второй же вариант будет правильнее назвать развить интуицию. Как только вы узнаете, как интерфейсы или слайсы работают изнутри, интуиция будет подсказывать правильнее и поможет избегать ошибок. Этот метод хорошо помог мне и, наверняка, поможет и другим. Поэтому я решил собрать эти базовые знания о внутренностях Go в один пост, чтобы помочь другим развить интуицию о том, как Go устроен изнутри.
Давайте начнем с базового понимания, как хранятся типы данных в памяти. Вот краткий перечень того, что мы изучим:
Указатели
Go, имея в генеалогическом дереве язык С, на самом деле довольно близок к железу. Если вы создаете переменную типа int64 (целочисленной значение 64-бита) вы точно можете быть уверены в том, сколько именно места она занимает в памяти, и всегда можете использовать unsafe.Sizeof(), чтобы узнать это для любого другого типа.
Я очень люблю использовать визуальное представление данных в памяти, чтобы «увидеть» размеры переменных, массивов или структур данных. Визуальный подход помогает быстрее понять масштабы, развить интуицию и наглядно оценивать даже такие вещи, как производительность.
Например, давайте начнём с простейших базовых типов в Go:
Скажем, в такой визуализации видно, что переменная типа int64 будет занимать в два раза больше «места», чем int32, а int занимает столько же, сколько int32 (подразумевая, что это 32-битная машина).
Указатели же выглядят чуть более сложно — по сути, это один блок памяти, который содержит адрес в памяти, указывающий на другой блок памяти, где лежат данные. Если вы слышите фразу «разыменовать указатель», то это означает «найти данные из блока памяти, на который указывает адрес в блоке памяти указателя». Можно представить это как-нибудь так:
Адрес в памяти обычно указывается в шестнадцатеричной форме, отсюда «0x. » на картинке. Но важный момент тут в том, что «блок памяти указателя» может быть в одном месте, а «данные, на которые указывает адрес» — совсем в другом. Нам это пригодится чуть дальше.
И тут мы подходим к одной из gotchas в Go, с которой сталкиваются люди, у которых не было опыта работы с указателями в других языках — это путаница в понимании что такое «передача по значению» параметров в функции. Как вы, наверняка знаете, в Go всё передаётся «по значению», тоесть буквально копируется. Давайте попробуем это визуализировать для функций, в которых параметр передаётся как есть и через указатель:
В первом случае мы копируем все эти блоки памяти — и, в реальности, их может быть запросто больше, чем 2, хоть 2 миллиона блоков, и они все будут копироваться, а это одна из самых дорогостоящих операций. Во втором же случае, мы копируем лишь один блок памяти — в котором хранится адрес в памяти — и это быстро и дешево. Впрочем, для небольших данных рекомендуется всё же передавать по значению, потому что у поинтеры создают дополнительную нагрузку на GC, и, в итоге оказываются более дорогими, но об этом как-нибудь в другой статье.
Окей, разогрев окончен, давайте копнём глубже и посмотрим вещи чуть сложнее.
Массивы и слайсы
Слайсы поначалу принимают за обычный массив. Но это не так, и, на самом деле, это два разных типа в Go. Давайте сначала посмотрим на массивы.
Массивы
Массив это просто последовательный набор блоков памяти и если мы посмотрим на исходники Go (src/runtime/malloc.go), то увидим, что создание массива это по сути просто выделение куска памяти нужного размера. Старый добрый malloc, только чуть умнее:
Что это для нас означает? Это значит, что мы можем визуально представить массив просто как набор блоков памяти, расположенных один за другим:
То просто берем пятый (4+1) элемент и изменяем значение этого блока в памяти:
Окей, теперь разберёмся со слайсами.
Слайсы
На первый взгляд, они похожи на массивы. Ну вот прям очень похожи:
Но если мы посмотрим на исходники Go (src/runtime/slice.go), то увидим, что слайс это, на самом деле, структура из трёх полей — указателя на массив, длины и вместимости (capacity):
Когда вы создаёте новый слайс, рантайм «под капотом» создаст новую переменную этого типа, с нулевым указателем ( nil ) и длиной и ёмкостью равными нулю. Это нулевое значение для слайса. Давайте попробуем визуализировать его:
Это не очень интересно, поэтому давайте инициализируем слайс нужного нам размера с помощью встроенной команды make() :
Давайте посмотрим на оба вызова:
Теперь, объединяя наши знания о том как устроены указатели, массивы и слайсы, давайте визуализируем, что происходит при вызове следующего кода:
Это было легко. Но что будет, если мы создадим новый подслайс из foo и изменим какой-нибудь элемент? Давайте посмотрим:
И, скажем, считав 10МБ данных в слайс из файла, найти 3 байта, содержащих цифры, но возвращать вы будете слайс, который ссылается на массив размером 10МБ!
И это одна из самых часто упоминаемых gotchas в Go. Но теперь, наглядно понимая как это устроено, вам будет тяжело сделать такую ошибку.
Добавление к слайсу (append)
Взглянем на следующий код:
Он создаёт новый слайс из 32 целых чисел и добавляет к нему ещё один, 33-й элемент.
Помните про cap — ёмкость слайсов? Ёмкость означает количество выделенного места для массива. Функция append() проверяет, достаточно ли у слайса места, чтобы добавить туда ещё элемент, и если нет, то выделяет больше памяти. Выделение памяти это всегда дорогая операция, поэтому append() пытается оптимизировать это, и запрашивает в данном случае памяти не для одной переменной, а для ещё 32х — в два раза больше, чем начальный размер. Выделение памяти пачкой один раз дешевле, чем много раз по кусочкам.
Неочевидная штука тут в том, что по различным причинам, выделение памяти обычно означает выделение её по другому адресу и перемещение данных из старого места в новое. Это означает, что адрес массива, на который ссылается слайс также изменится! Давайте визуализируем это:
Мы получим вот это:
Именно так, мы получим два различных массива, и два слайса будут указывать на совершенно разные участки памяти! И это, мягко говоря, довольно контринтуитивно, согласитесь. Поэтому, как правило, если вы вы работаете с append() и подслайсами — будьте осторожны и имейте ввиду эту особенность.
К слову, append() увеличивает слайс удвоением только до 1024 байт, а затем начинает использовать другой подход — так называемые «классы размеров памяти», которые гарантируют, что будет выделяться не более
12.5%. Выделять 64 байта для массива на 32 байта это нормально, но если слайс размером 4ГБ, то выделять ещё 4ГБ даже если мы хотим добавить лишь один элемент — это чересчур дорого.
Интерфейсы
Окей, интерфейсы, наверное, самая непонятная штука в Go. Обычно проходит какое-то время, прежде чем понимание укладывается в голове, особенно после тяжелых последствий долгой работы с классами в других языках. И одна из самых популярных проблем это понимание nil интерфейса.
Как обычно, давайте обратимся к исходному коду Go. Что из себя представляет интерфейс? Это обычная структура из двух полей, вот её определение (src/runtime/runtime2.go):
itab означает interface table и тоже является структурой, в которой хранится дополнительная информация об интерфейсе и базовом типе:
Мы сейчас не будем углубляться в то, как работает приведение типа в интерфейсах, но что важно понимать, что по своей сути интерфейс это всего лишь набор данных о типах (интерфейса и типа переменной внутри него) и указатель на, собственно, саму переменную со статическим (конкретным) типом (поле data в iface ). Давайте посмотрим, как это выглядит и определим переменную err интерфейсного типа error :
Теперь, взгляните на вот этот случай, который также является известной gotcha в Go:
Теперь вам должно быть сложно натолкнуться на такую проблему в вашем коде.
Пустой интерфейс (empty interface)
Одна из известных непоняток с пустым интерфейсом заключается в том, что нельзя одним махом привести слайс конкретных типов к слайсу интерфейсов. Если вы напишете что-то вроде такого:
Комплиятор вполне недвусмысленно ругнётся:
Поначалу это сбивает с толку. Мол, что за дела — я могу привести одну переменную любого типа в пустой интерфейс, почему же нельзя сделать тоже самое со слайсом? Но, когда вы знаете, что из себя представляет пустой интерфейс и как устроены слайсы, то вы должны интуитивно понять, что это «приведение слайса» на самом деле — довольно дорогая операция, которая будет подразумевать проход по всей длине слайса и выделение памяти прямо пропорционального количеству элементов. А, поскольку один из принципов в Go это — хотите сделать что-то дорогое — делайте это явно, то такая конвертация отдана на откуп программисту.
Давайте попробуем визуализировать, что собой представляет приведение []int в []interface<> :
Надеюсь, теперь этот момент имеет смысл и для вас.
Заключение
Безусловно, не все gotchas и непонятки языка можно решить, углубившись во внутренности реализации. Некоторые из них являются просто разницей между старым и новым опытом, а он у нас всех различен. И всё же, наиболее популярные из них такой подход помогает обойти. Надеюсь этот пост поможет вам глубже разобраться в том, что происходит в ваших программах и как Go устроен под капотом. Go это ваш друг, и знать его чуть лучше всегда будет на пользу.
Если вам интересно почитать больше о внутренностях Go, вот небольшая подборка статей, которые помогли в своё время мне:
Массивы и срезы в Go
Published on January 24, 2020
Введение
В Go массивы и *срезы *представляют собой структуры данных, состоящие из упорядоченных последовательностей элементов. Эти наборы данных очень удобно использовать, когда вам требуется работать с большим количеством связанных значений. Они позволяют хранить вместе связанные данные, концентрировать код и одновременно применять одни и те же методы и операции к нескольким значениям.
Хотя и массивы, и срезы в Go представляют собой упорядоченные последовательности элементов, между ними имеются существенные отличия. Массив в Go представляет собой структуру данных, состоящую из упорядоченной последовательности элементов, емкость которой определяется в момент создания. После определения размера массива его нельзя изменить. Срез — это версия массива с переменной длиной, дающая разработчикам дополнительную гибкость использования этих структур данных. Срезы — это то, что обычно называют массивами в других языках.
С учетом этих отличий массивы и срезы предпочтительнее использовать в определенных ситуациях. Если вы только начинаете работать с Go, вам может быть сложно определить, что использовать в каком-либо конкретном случае. Благодаря универсальному характеру срезов, они будут полезнее в большинстве случаев, однако в некоторых ситуациях именно массивы могут помочь оптимизировать производительность программы.
В этой статье мы подробно расскажем о массивах и срезах и предоставим вам информацию, необходимую для правильного выбора между этими типами данных. Также вы узнаете о наиболее распространенных способах декларирования массивов и срезов и работы с ними. Вначале мы опишем массивы и манипуляции с ними, а затем расскажем о срезах и их отличиях.
Массивы
Массивы представляют собой структурированные наборы данных с заданным количеством элементов. Поскольку массивы имеют фиксированный размер, память для структуры данных нужно выделить только один раз, в то время как для структур данных переменной длины требуется динамическое выделение памяти в большем или меньшем объеме. Хотя из-за фиксированной длины массивов они не отличаются гибкостью в использовании, одноразовое выделение памяти позволяет повысить скорость и производительность вашей программы. В связи с этим, разработчики обычно используют массивы при оптимизации программ, в том числе, когда для структур данных не требуется переменное количество элементов.
Определение массива
Ниже показана общая схема декларирования массива:
Примечание: важно помнить, что в каждом случае декларирования нового массива создается отдельный тип. Поэтому, хотя [2]int и [3]int содержат целочисленные элементы, из-за разницы длины типы данных этих массивов несовместимы друг с другом.
Например, следующий массив numbers имеет три целочисленных элемента, у которых еще нет значения:
Если вы хотите назначить значения элементов при создании массива, эти значения следует поместить в фигурные скобки. Массив строк с заданными значениями будет выглядеть следующим образом:
Вы можете сохранить массив в переменной и вывести его:
Запуск программы с вышеуказанными строчками даст следующий результат:
Результат будет выглядеть следующим образом:
Теперь все элементы заключены в кавычки. Оператор \n предписывает добавить в конце символ возврата строки.
Теперь вы понимаете основные принципы декларирования массивов и их содержание, и мы можем перейти к изложению того, как задавать элементы в массиве по номеру индекса.
Индексация массивов (и срезов)
В следующих примерах мы будем использовать массив, однако данные правила верны и для срезов, поскольку индексация массивов и срезов выполняется одинаково.
Для массива coral индекс будет выглядеть следующим образом:
“blue coral” | “staghorn coral” | “pillar coral” | “elkhorn coral” |
---|---|---|---|
0 | 1 | 2 | 3 |
Поскольку каждый элемент среза или массива имеет соответствующий ему номер индекса, мы можем получать доступ к этим элементам и выполнять с ними манипуляции точно так же, как и с другими последовательными типами данных.
Теперь мы можем вызвать дискретный элемент среза по его номеру индекса:
При индексации массива или среза всегда следует использовать положительные числа. В отличие от некоторых языков, поддерживающих обратную индексацию с использованием отрицательных чисел, в Go такая индексация приводит к ошибке:
Мы можем объединять элементы строк массива или среза с другими строками, используя оператор + :
Поскольку номера индекса соответствуют элементам массива или среза, мы можем получать отдельный доступ к каждому элементу и работать с этими элементами. Чтобы продемонстрировать это, мы покажем, как можно изменить элемент с определенным индексом.
Изменение элементов
Мы можем использовать индексацию для изменения элементов массива или среза, задав для индексированного элемента другое значение. Это дает нам дополнительные возможности контроля данных в срезах и массивах и позволяет программно изменять отдельные элементы.
Теперь вы знаете, как выполнять манипуляции с отдельными элементами массива или среза, и мы рассмотрим несколько функций, дающих дополнительную гибкость при работе с типами данных в коллекциях.
Подсчет элементов с помощью len()
Если вы создадите массив целых чисел с большим количеством элементов, вы можете использовать функцию len() и в этом случае:
Результат будет выглядеть следующим образом:
Хотя примеры массивов содержат относительно немного элементов, функция len() особенно полезна при определении количества элементов внутри очень больших массивов.
Далее мы рассмотрим процедуру добавления элемента в тип данных коллекции и покажем, как это сделать, поскольку в связи с фиксированной длиной массивов добавление таких статических типов данных может повлечь за собой ошибку.
Добавление элементов с помощью append()
append() — это встроенный метод Go для добавления элементов в тип данных коллекции. Но данный метод не будет работать с помощью массива. Как уже отмечалось, основное отличие массивов от срезов заключается в том, что размер массива нельзя изменить. Это означает, что хотя вы можете изменять значения элементов в массиве, вы не можете сделать массив больше или меньше после его определения.
Рассмотрим наш массив coral :
В результате вы получите сообщение об ошибке:
Для решения подобных проблем нам нужно больше узнать о типе данных среза, определении среза и процедуре конвертации массива в срез.
Срезы
Срез *— это тип данных Go, представляющий собой *мутируемую или изменяемую упорядоченную последовательность элементов. Поскольку размер срезов не постоянный, а переменный, его использование сопряжено с дополнительной гибкостью. При работе с наборами данных, которые в будущем могут увеличиваться или уменьшаться, использование среза обеспечит отсутствие ошибок при попытке изменения размера набора. В большинстве случаев возможность изменения стоит издержек перераспределения памяти, которое иногда требуется для срезов, в отличие от массивов. Если вам требуется сохранить большое количество элементов или провести итерацию большого количества элементов, и при этом вам нужна возможность быстрого изменения этих элементов, вам подойдет тип данных среза.
Определение среза
Срезы определяются посредством декларирования типа данных, перед которым идут пустые квадратные скобки ( [] ) и список элементов в фигурных скобках ( <> ). Вы видите, что в отличие от массивов, для которых требуется поставить в скобки значения int для декларирования определенной длины, в срезе скобки пустые, что означает переменную длину.
Создадим срез, содержащий элементы строкового типа данных:
При выводе среза мы видим содержащиеся в срезе элементы:
Результат будет выглядеть следующим образом:
Если вы хотите создать срез определенной длины без заполнения элементов коллекции, вы можете использовать встроенную функцию make() :
При печати этого среза вы получите следующий результат:
Если вы хотите заранее выделить определенный объем памяти, вы можете использовать в команде make() третий аргумент:
При этом будет создан обнуленный срез с длиной 3 и заранее выделенной емкостью в 5 элементов.
Разделение массивов на срезы
Запуск программы с этой строкой даст следующий результат:
При создании среза (например, [1:3] ), первое число означает начало среза (включительно), а второе число — это сумма первого числа и общего количества элементов, которое вы хотите получить:
В этом случае вы вызываете второй элемент (или индекс 1) в качестве начальной точки, а всего вызываете два элемента. Результат будет выглядеть следующим образом:
Вот как это было получено:
В результате будет выведено следующее:
Чтобы включить все элементы до конца массива, нужно использовать обратный синтаксис:
Получившийся срез будет выглядеть следующим образом:
В этом разделе мы рассказали о вызове отдельных частей массива посредством разделения массива на срезы. Далее мы расскажем, как преобразовывать полные массивы в срезы.
Преобразование массива в срез
Если вы создали массив и считаете, что для него требуется переменная длина, вы можете преобразовать этот массив в срез. Чтобы преобразовать массив в срез, используйте процесс разделения на срезы, описанный в разделе Разделение массивов на срезы настоящего документа, но пропустите указание обоих числовых индексов, определяющих конечные точки:
Учтите, что вы не сможете конвертировать саму переменную coral в срез, поскольку после определения переменной в Go ее тип нельзя изменить. Чтобы обойти эту проблему, вы можете скопировать полное содержание массива в новую переменную в качестве среза:
Теперь попробуйте добавить элемент black coral как в разделе массива, используя функцию append() в новом конвертированном срезе:
В результате будет выведен срез с добавленным элементом:
В одном выражении append() можно добавить несколько элементов:
Вы научились добавлять элементы в срез, а теперь мы покажем, как удалять их из срезов.
Удаление элемента из среза
В отличие от других языков, в Go отсутствуют встроенные функции для удаления элементов из среза. Для удаления элементов из среза их нужно вырезать.
Чтобы удалить элемент, нужно выделить в срез элементы до него, затем элементы после него, а затем объединить два новых среза в один срез, не содержащий удаленного элемента.
Если i — индекс удаляемого элемента, формат этого процесса будет выглядеть следующим образом:
Теперь вы знаете, как добавлять и удалять элементы среза, и мы перейдем к измерению объема данных, который может храниться в срезе в любой момент времени.
Измерение емкости среза с помощью cap()
Поскольку срезы имеют переменную длину, для определения размера этого типа данных метод len() подходит не очень хорошо. Вместо него лучше использовать функцию cap() для определения емкости слайса. Данная функция показывает, сколько элементов может содержать срез. Емкость определяется объемом памяти, который уже выделен для этого среза.
Примечание: поскольку длина и емкость массива всегда совпадают, функция cap() не работает с массивами.
Функция cap() обычно используется для создания среза с заданным числом элементов и заполнения этих элементов с помощью программных методов. Это позволяет предотвратить выделение лишнего объема памяти при использовании команды append() для добавления элементов сверх выделенной емкости.
Вначале рассмотрим использование append() :
Теперь заполним срез без использования append() посредством выделения определенной длины / емкости:
Построение многомерных срезов
Также вы можете определять срезы, содержащие в качестве элементов другие срезы, при этом каждый список в скобках содержится также в скобках родительского среза. Такие наборы срезов называются многомерными срезами. Их можно представить как описание многомерных координат. Например, набор из пяти срезов, каждый из которых содержит шесть элементов, можно представить как двухмерную сетку с длиной пять и высотой шесть.
Рассмотрим следующий многомерный срез:
Чтобы получить доступ к элементу этого среза, нам нужно использовать несколько индексов, каждый из которых соответствует одному измерению конструкции:
Далее идут значения индекса для остальных отдельных элементов:
При работе с многомерными срезами важно помнить, что для доступа к конкретным элементам вложенного среза нужно ссылаться на несколько числовых индексов.
Заключение
В этом обучающем руководстве вы изучили основы работы с массивами и среза в Go. Вы выполнили несколько упражнений, демонстрирующих отличия между массивами с фиксированной длиной и срезами с переменной длиной, и определили, как эти отличия влияют на ситуативное использование этих структур данных.
Чтобы продолжить изучение структур данных в Go, ознакомьтесь со статьей Карты в Go или со всей серией статей по программированию на языке Go.