Массивы и слайсы в Go — для собеседований

от автора

Набив несколько шишек поначалу мы начинаем довольно уверенно пользоваться массивами и слайсами в Go и обычно не сильно задумываемся над разными неприятными подробностями (если пишем достаточно аккуратно). Однако когда дело доходит до собеседований, оказывается что наши интуитивные представления легко могут дать сбой — где-то что-то забылось, а о каких-то нюансах может и не задумывались.

Здесь собраны несколько базовых вопросов встретившихся в последнюю сессию поисков работы 🙂 некоторые могут быть тривиальны — но трудно ведь угадать у кого на каком вопросе может быть пробел. А может и поможет тем кто только вникает в язык. Местами дополнены подробности из мануалов. Слишком подробных ответов будем избегать чтобы не стало скучно — найти их несложно.

Желательно не использовать этот список в качестве вопросов на собеседовании. Некоторые из них могут создать о вас странное впечатление у кандидата 🙂

Для начала: что такое массив и что такое слайс

И как их отличить? Вспоминаем что в Go массив это именно массив — последовательность однотипных элементов заданной (и неизменяемой) длины — но зачастую мы оперируем не массивами а слайсами с них.

Слайс есть вещь легковесная — только указатель на «подложенный» под него массив, причем необязательно с начала — и длина. Массив под слайсом может быть «неявным» — то есть у него нет «своей» переменной. В то же время на одном массиве как грибы могут расти несколько слайсов, в том числе пересекающихся.

Кроме длины есть у слайса ещё «capacity» (вместимость) — она относится именно к слайсу хотя зависит от нижележащего массива. Лучше потом посмотрим на примерах.

Функция len(…)

Про len(…) все знают — она возвращает длину строки, массива, слайса или размер мэпы. А к чему кроме массивов, строк, слайсов или мэп её можно применить?

К указателю на массив и к каналу (!). А к указателю на слайс или мэпу нельзя (это можно объяснить но может быть нелегко запомнить).

Также len(…) нормально проглатывает nil-ы если они имеют один из вышеуказанных типов.

И функция cap(…)

Можно годами жить и не знать про неё. Она возвращает капасити слайса. Кроме слайса можно её вызвать на массиве, хотя смысла в этом нет. Когда она нужна? не думаю что вы легко придумаете хорошие кейсы 🙂

Для самоконтроля, проверьте, чему равны капасити слайсов в коде ниже:

a := []int{2, 3, 5, 7, 9} println(cap(a)) b := a[1:4] println(cap(b))

Про функцию make(…)

Наверное один из первых вопросов — что можно сделать с её помощью (слайс, мэпу или канал). При этом вторым аргументом идёт размер — для слайса обязательный, для мэпы и канала опциональный. Для мэпы он определяет начальное количество «бакетов», но поскольку она растёт автоматически, об этом нечасто вспоминают. Для слайсов можно указать и третий аргумент — капасити (т.е. размер безымянного массива под данным слайсом).

a := make([]int, 3, 5) fmt.Printf("%v %d %d\n", a, len(a), cap(a))

Думаю, мы часто создаём слайсы просто как a := []int{} с целью дальнейшего аппенда. А какая у него будет капасити? Не стоит гадать 🙂

Что будет если вылезти за пределы слайса?

Да паника будет — кажется, очевидный ответ, наверняка сталкивались 🙂

a := []int{2, 3, 5, 7, 9} b := a[1:4] println(b[3])      // паника, т.к. длина этого слайса всего 3

однако…

Что за пределами слайса, если очень хочется?

Без паники! Слайс можно покастить к слайсу бОльшей длины

a := []int{2, 3, 5, 7, 9} b := a[1:4] println(b[:4][3])      // печатает 9 из массива под слайсом

однако…

за капасити вылезти всё равно нельзя, b[:6] в этом примере вызовет панику

Неаккуратный append(…)

Классика наверное — append может модифицировать нижележащий массив если есть капасити. Нечасто на это наткнёшься, но особенно при передаче в функцию — можно:

func sum(a []int) int {     // somewhat artistic way to do this simple task a = append(a, 0) s := 0 for a[0] > 0 { s += a[0] a = a[1:] } return s }  func main() { primes := []int{2, 3, 5, 7, 11, 13} println(sum(primes[0:3])) println(sum(primes[2:5])) }

Внутри функции мы аппендим к слайсу нолик — вроде ничего, ведь переменная «a» локальна, саму её можно менять сколько угодно. Конечно для суммы такой изощрённый код писать мы вряд ли будем — но в иных ситуациях соблазн дописать что-то в конец чтобы упростить обработку «краевых условий» бывает велик.

А как растёт слайс?

То есть, если append(…) все же вылезает за капасити. И выделяется новый массив (неявный), под слайс, возвращаемый как результат append-а. И в него копируются элементы. Вот какого размера этот новый массив (и капасити слайса)? Несложно проверить:

a := make([]int, 7) a = append(a, 13) fmt.Printf("%d %d\n", len(a), cap(a)) // печатает 8 и 14

итак, размер удваивается — это поведение можно найти в подобных случаях и в других языках, однако не стоит об этом говорить как о непреложной истине — конечно, это детали реализации, это не специфицировано. Отдельный нюанс — если слайс изначально имел capacity=0. Но в целом нас это интересует лишь постольку поскольку слайс на 100 миллионов элементов при добавлении всего одного числа может потребовать памяти на 300 миллионов (т.к. на время копирования нужно чтобы в памяти были и старый и новый массив).

Как аппендить целый слайс а не одиночный элемент?

Просто нужно помнить что append позволяет добавить произвольное количество элементов через запятую (variadic arguments) — и есть волшебный синтаксис с «многоточием», разворачивающий слайс в такую последовательность аргументов:

b := []int{1, 2, 3, 5, 8} a := append(a, b...)

Есть ли ограничение на длину слайса, добавляемого таким образом? Ведь казалось бы аргументы функции — как и локальные переменные выделяются на стеке. Но во-первых стек горутины увеличивается по необходимости — во-вторых variadic аргументы на самом деле передаются с помощью слайса — то есть это «синтаксический сахар», а не хардкорная реализация как в С.

Функция clear(…)

Ещё одна функция о которой спокойно можно не знать. И может даже лучше не знать. Она очищает мэпы и слайсы. А к массиву её применить нельзя (логика?) — хотя можно если покастить в слайс той же длины (см ниже).

Причем мэпа просто становится пустой, а в слайсе проставляются «нулевые» значения данного типа, что может быть слегка неочевидно.

Функция copy(…)

Из той же оперы: функция, копирующая слайс в слайс. Почему бы не сделать функцию «клонирующую» слайс — непонятно. Из нюансов — она умеет копировать строку в слайс байт. Также отдельно подчёркивается что слайсы могут пересекаться но не сказано, какого результата мы при этом ожидаем. Видимо у авторов реминисценции по поводу strcpy(…) из языка С, которая при неаккуратном использовании в этом случае могла привести к затиранию признака конца строки с последующим segfault или порчей других переменных.

a := []int{2, 3, 5, 7, 11} copy(a[0:2], a[2:4]) fmt.Printf("%v\n", a)

Можете попробовать угадывать какой массив получается в результате (а что если переставить аргументы функции местами?) — хотя если я правильно понимаю, раз не указано, то результат может зависеть даже от версии.

Функции min(…) и max(…)

Встроенные функции появившиеся кажется с 1.21 версии. Вообще-то у них variadic аргументы, то есть в основном вы их применяете например для выбора меньшего из двух. Но как подсказано выше — можно же развернуть и слайс:

minOfTwo := min(8, 13)  a := []int{3, 1, 4, 1, 5, 9} maxOfList := max(a...)

Может ли функция возвращать массив (а не слайс)

Конечно может — хотя на практике это увидишь нечасто (может поэтому и пытаются «подловить»?) Случаи когда нужно вернуть данные фиксированного размера встречаются например при подсчете хэшей. Как пример — в crypto/md5:

const Size = 16 ... func Sum(data []byte) [Size]byte

Как покастить массив в слайс

Например, чтобы использовать его в clear(…) или append(…) — или использовать результат функции хэширования (выше) там где нужен слайс.

Вообще это очевидно — просто взять с него слайс размером с весь массив. Задавая этот вопрос то ли врасплох пытаются поймать, то ли надеятся что кандидат намудрит с синтаксисом:

arr := md5.Sum([]byte("I'm a fine string")) slice1 := arr[0:len(arr)] // если мы забыли что начало и конец можно не указывать slice2 := arr[:] // вот так норм

Из этого вопроса следует ещё один, идеологический и абстрактный

Можно ли было оставить Go без массивов вообще

Имеется в виду — использовать только слайсы под которыми выделены неявные массивы. Я на этот вопрос хмыкнул и ответил утвердительно, поскольку не припомню (и не придумаю случая) когда нужен строго массив или когда слайс будет мешать. Интервьюер тоже хмыкнул и сказал что-то неопределенное вроде «угу» — вероятно сам тоже не мог ни припомнить ни придумать. Зачем спрашивал? чисто «посмотреть как собеседник рассуждает»?

Пожалуй на этом остановимся — всем успехов! Смело добавляйте, поправляйте, критикуйте!


ссылка на оригинал статьи https://habr.com/ru/articles/848392/