В статье я хотел бы рассмотреть массивы, слайсы и строки в Golang и их особенности. Я стараюсь приводить примеры вопросов и заданий, которые могут встретиться вам на собеседовании на должность backend-разработчика, где предполагается знание языка Go. Практически все это вы сможете найти в других источниках, но в статье я постарался собрать в одном месте и отсеять то, что, на мой взгляд, является второстепенным, чтобы уменьшить количество материала и обратить внимание читателя на более основные и важные моменты. Для более детального изучения вы сможете воспользоваться ссылками на дополнительные материалы, приведенные в статье.
Массивы (arrays)
Сразу хочу сказать, что вопросы по массивам встречаются на интервью редко. Однако их понимание необходимо, потому что они являются базой для слайсов и строк. Массив в Go не отличается по своей сути от массивов в общепринятом смысле – это структура данных, которая содержит от 0 до N элементов определенного (заданного) типа. Количество элементов массива указывается при его создании.
Особенности массива:
- память под массив выделяется в процессе создания.
- размер массива поменять невозможно.
- два массива разной размерности, но с элементами одного типа – это два разных массива (разных типа). Следовательно, их невозможно сравнить между собой с помощью операторов
==
и!=
- по умолчанию все элементы массива инициализируются нулевыми значениями заданного типа.
Примеры:
Срезы (slices)
Срез можно рассматривать как динамический массив. Это значит, что вы можете изменять его размер.
Срез представляет собой структуру, в которой содержится указатель на начало области памяти (массива), длина слайса (length) и объем области памяти (capacity)
В коде Golang slice определен как структура с указателем на массив, длиной и емкостью: https://github.com/golang/go/blob/master/src/runtime/slice.go#L15
Важной особенностью такого представления является то, что на одну и ту же область памяти с данными (массив) может ссылаться несколько слайсов. Например, как показано на рисунке:
Такое может получиться в результате операции
re-slicing'а. Для этого используется запись:
newSlice = originalSlice[firstElementIndex:lastElementIndex]
.
Обратите внимание, что lasElementIndex
не
включается в новый слайс (т. е. в слайсе
будут элементы от firstElementIndex
до
lastElementIndex-1
включительно): пример на playground.
Если
не указывать firstElementIndex
, то он будет
равен первому элементу, если не указывать lastElementIndex
, то он будет равен длине слайса: пример на playground.
Особенности среза:
- нулевое значение слайса:
nil
и к нему можно применять функциюappend
для добавления элементов (пример на playground). - при создании с помощью
make
можно указать capacity (третьим аргументом). - особенности работы
append
: при достаточном capacity увеличивается length слайса, если места не хватает – происходит перевыделение памяти и копирование данных. Новый слайс указывает на новую область памяти с новой длиной (length) и обьемом (capacity). Обычно говорят, что capacity увеличивается в 2 раза (на 100%), но это верно пока количество элементов в слайсе менее 512. После этого увеличение размера плавно уменьшается до 25% (пример на playground). Логику работыappend
можно посмотреть в коде golang. - стоит учитывать, что слайс хотя и является структурой, но содержит внутри ссылку и поэтому при рейслайсинге или передаче слайса в функцию и изменении данных слайса в новом слайсе или внутри функции они будут изменены и в оригинальном слайсе, так как указывают на одну область памяти. Чтобы избежать такого поведения, можно воспользоваться функцией
copy
– она скопирует данные в новый слайс (пример на playground). - однако относительно предыдущего пункта стоит учитывать, что если функция
append
расширила область памяти (не хватило capacity и была выделена дополнительная память), то старый слайс будет указывать на старую область, а новый – на новую. Тогда изменения в одном из них не приведут к изменениям в другом (пример на playground). Но стоит помнить, что не всегда приappend
происходит перевыделение памяти. - слайсы (как и массивы) – одномерные. Для создания двумерного слайса нужно создать слайс слайсов (пример на playground).
- cлайсы можно сравнивать только с
nil
. В остальных случаях можно использоватьreflect.DeepEqual
(пример на playground).
Примеры работы со срезами
- Создать слайс можно разными способами (см. пример), при использование
make
можно указать capacity (обратите внимание на значения по умолчанию):
- Для добавления элемента(-ов) в слайс используется функция
append
:
- И можно итерировать по слайсу с помощью
range
.
Примеры задач
Дополнительные материалы
- A tour of Go: Slices
- A Comprehensive Guide of Arrays and Slices in Golang (and their differences) (или перевод на habr)
- SliceTricks
Строки
Строка представляет собой слайс байтов и является неизменяемой. Это значит, что вы не можете поменять отдельный байт в строке после ее объявления. Однако стоит сказать, что строка может быть представлена в различной кодировке и один символ не обязательно соответствует одному байту (это зависит от символа и используемой кодировки).
Особенности строк:
- строка содержит неизменяемые байты, ее длина и сами байты не могут быть изменены после объявления.
- доступ по индексу – это доступ к байту, а не к символу (так как символ может занимать более одного байта).
- исходный код в Go использует кодировку UTF-8, поэтому строки обычно представлены в этой кодировке (если значения строк заданы в тексте программы).
rune
– специальный тип в Go, который представляет символ в формате UTF-8.- для итерации по
runes
(рунам) можно использовать операторrange
- для работы с UTF-8 можно использовать пакет
unicode/utf8
из стандартной библиотеки.
Примеры работы со строками
Примеры задач
Дополнительные материалы
- Strings, bytes, runes and characters in Go
- Go Walkthrough: bytes + strings packages (или перевод на habr)
Расскажите, какие вопросы вам задавали на интервью на Go-разработчика?