Набив несколько шишек поначалу мы начинаем довольно уверенно пользоваться массивами и слайсами в 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...)
обидно, но это не работает (как корректно напомнили в комментарии - а я третий раз забываю) - во-первых первый аргумент там выделенный, но не сработает даже в варианте `max(a[0], a...)` - это волшебные функции и вариадик в них не такой как везде :(
Используйте slices.Max(...)
и slices.Min(...)
Может ли функция возвращать массив (а не слайс)
Конечно может - хотя на практике это увидишь нечасто (может поэтому и пытаются "подловить"?) Случаи когда нужно вернуть данные фиксированного размера встречаются например при подсчете хэшей. Как пример - в 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 без массивов вообще
Имеется в виду - использовать только слайсы под которыми выделены неявные массивы. Я на этот вопрос хмыкнул и ответил утвердительно, поскольку не припомню (и не придумаю случая) когда нужен строго массив или когда слайс будет мешать. Интервьюер тоже хмыкнул и сказал что-то неопределенное вроде "угу" - вероятно сам тоже не мог ни припомнить ни придумать. Зачем спрашивал? чисто "посмотреть как собеседник рассуждает"?
Пожалуй на этом остановимся - всем успехов! Смело добавляйте, поправляйте, критикуйте!
Комментарии (15)
gudvinr
05.10.2024 07:58+1Можно ли было оставить Go без массивов вообще
Массивы в го - бесплатные. Т.е. на них не тратятся дополнительные ресурсы.
Для слайсов в любом случае пришлось бы выделять кусок памяти и хранить на него указатель. Массив - это и есть просто сырой кусок памяти.
Каждый слайс - это +16 байт. Это может мешать, если у вас очень много мелких сущностей - это конечно случай граничный, но не нереалистичный. Пример - IP адреса, хеши, да и вообще всё у чего не меняется размер. Для IPv4 заголовок слайса занимает большую часть хранилища.
Не совсем очевидный момент, но массивы лучше переживают escape to heap. По крайней мере раньше можно было выделить массив большего размера, прежде чем он вытесняется в кучу.
Sly_tom_cat
05.10.2024 07:58Хедер слайса: поинтер на массив, длинна и капасити. Поинтер и int (длинна и капасити) - соответствуют разрядности системы.
Для 64 битной архитектуры это 64 + 64 + 64 = 192 бита или 24 байта
Для 32 бит - 12 байт.
Откуда +16 байт у вас взялись - не очень понятно.gudvinr
05.10.2024 07:58поинтер на массив
Откуда +16 байт у вас взялись - не очень понятно
Вы уже подошли очень близко к разгадке.
В слайсе лежит указатель на массив. Если оперировать указателями на массивы (а такое может быть, если вам не хочется их копировать), так или иначе у вас под указатель все равно где-то будут выделяться переменные. А длина (с одной н) и ёмкость - они всегда лишние, если в слайсе/массиве хранятся данные с неизменяемой длиной.
Если вы все равно захотите придраться, что в слайсе на каждый объект хранится указатель, а при работе с массивами - не факт, то я вам отвечу что вы упустили основную деталь. Не важно сколько именно лишних байт, важно то, что они лишние.
В пресловутом случае с IP адресами уже одно поле длины в два раза больше, чем IPv4. Поэтому концептуально нет разницы, во сколько раз слайс больше памяти занимает - в 4 раза, в 6 или в 3 на 32-битной системе.
Sly_tom_cat
05.10.2024 07:58Вы реально не поняли о чем я.
Массив (как бы вы с ним ни работали) останется массивом и "под" слайсом, но нужны там дина и емкость (пусть будет по грамотному если для вас это так важно) или не нужны, а заголовок слайса все равно будет создан и он займет именно 24/12 байт в зависимости от разрядности системы.Если считать что переменная это указатель на массив, но в таком случае и переменная типа слайса - это тоже указатель, только на заголовок слайса (а уже тот ссылается на массив).
Т.о. разница между использованием массива и слайса - +24 или +12 байт но не +16.
А по остальным моментам я с вами и не спорил, и повторяться про IP адрес не стоило.
QtRoS
05.10.2024 07:58+3Стоит упомянуть про то, что массивы по семантике ближе к значениям, а слайсы к ссылкам. На практике это заметно в кейсах:
Передача в функцию - массивы копируются
Сравнение - массивы можно сравнивать через оператор ==
for-range - массивы неизменяемы в таком цикле из-за неявного копирования
RodionGork Автор
05.10.2024 07:58Здорово, спасибо! Честно скажу - абсолютно не обращал на это внимания сам - ну как во вступлении и сказано :)
lazy_val
05.10.2024 07:58+4итак, размер удваивается
Вообще-то удваивается до определенного предела capacity, а дальше в зависимости от размера исходного слайса применяется таблица коэффициентов:
starting cap growth factor
256 2.0
512 1.63
1024 1.44
2048 1.35
4096 1.30И это начиная с версии 1.18, а до этого просто начиная с capacity 1024 применялся множитель 1.25
RodionGork Автор
05.10.2024 07:58Классно, спасибо за уточнение. Наверняка это не специфицировано и может ещё раз (не раз) поменяться - но прекрасно как демонстрация факта что не стоит верить результатам поверхностных наблюдений :)
coctic
05.10.2024 07:58+2Про необходимость массивов.
Если читать какой-нибудь бинарный протокол при помощи binary.Read, то структура с членом-массивом читается без проблем, а если бы это были слайсы - как минимум перед чтением надо было проинициализировать размеры этих слайсов или как-то по-другому передать размеры. Можно, но с массивом гораздо удобнее и ошибкоустойчивее.
RodionGork Автор
05.10.2024 07:58функции возвращающие слайсы обычно инициализируют их внутри себя, с этим проблемы особой не видно.
coctic
05.10.2024 07:58А откуда он будет знать, сколько байт прочитать и в какой размер инициализировать слайс? Если массив - все понятно, размер берется из Reflection, сколько надо байт прочитали, сколько надо элементов записали.
А если там слайс, откуда размеры брать?
Bytamine
05.10.2024 07:58+2a := []int{3, 1, 4, 1, 5, 9}
maxOfList := max(a...)
Это не работает же.
RodionGork Автор
05.10.2024 07:58точно, это же "магические" функции, там нечестный variadic :(
спасибо, поправил - перепутал соslices.Max(...)
vtolstov
05.10.2024 07:58забавно, похожи на часть моих вопросов, но только на вопрос зачем вообще массивы, который я задаю - у меня есть ответ - работа с ними зачастую более эффективная.
Rezzet
У меня только один вопрос - зачем это вообще нужно? Выглядит как добавление лишней сущности.