Массивы и слайсы в 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)
Можете попробовать угадывать какой массив получается в результате (а что если переставить аргументы функции местами?) — хотя если я правильно понимаю, раз не указано, то результат может зависеть даже от версии.
Может ли функция возвращать массив (а не слайс)
Конечно может — хотя на практике это увидишь нечасто (может поэтому и пытаются «подловить»?) Случаи когда нужно вернуть данные фиксированного размера встречаются например при подсчете хэшей. Как пример — в 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 без массивов вообще
Имеется в виду — использовать только слайсы под которыми выделены неявные массивы. Я на этот вопрос хмыкнул и ответил утвердительно, поскольку не припомню (и не придумаю случая) когда нужен строго массив или когда слайс будет мешать. Интервьюер тоже хмыкнул и сказал что-то неопределенное вроде «угу» — вероятно сам тоже не мог ни припомнить ни придумать. Зачем спрашивал? чисто «посмотреть как собеседник рассуждает»?
Пожалуй на этом остановимся — всем успехов! Смело добавляйте, поправляйте, критикуйте!