Массивы и слайсы в Go — для собеседований
- воскресенье, 6 октября 2024 г. в 00:00:05
Набив несколько шишек поначалу мы начинаем довольно уверенно пользоваться массивами и слайсами в Go и обычно не сильно задумываемся над разными неприятными подробностями (если пишем достаточно аккуратно). Однако когда дело доходит до собеседований, оказывается что наши интуитивные представления легко могут дать сбой - где-то что-то забылось, а о каких-то нюансах может и не задумывались.
Здесь собраны несколько базовых вопросов встретившихся в последнюю сессию поисков работы :) некоторые могут быть тривиальны - но трудно ведь угадать у кого на каком вопросе может быть пробел. А может и поможет тем кто только вникает в язык. Местами дополнены подробности из мануалов. Слишком подробных ответов будем избегать чтобы не стало скучно - найти их несложно.
Желательно не использовать этот список в качестве вопросов на собеседовании. Некоторые из них могут создать о вас странное впечатление у кандидата :)
И как их отличить? Вспоминаем что в Go массив это именно массив - последовательность однотипных элементов заданной (и неизменяемой) длины - но зачастую мы оперируем не массивами а слайсами с них.
Слайс есть вещь легковесная - только указатель на "подложенный" под него массив, причем необязательно с начала - и длина. Массив под слайсом может быть "неявным" - то есть у него нет "своей" переменной. В то же время на одном массиве как грибы могут расти несколько слайсов, в том числе пересекающихся.
Кроме длины есть у слайса ещё "capacity" (вместимость) - она относится именно к слайсу хотя зависит от нижележащего массива. Лучше потом посмотрим на примерах.
Про len(...) все знают - она возвращает длину строки, массива, слайса или размер мэпы. А к чему кроме массивов, строк, слайсов или мэп её можно применить?
К указателю на массив и к каналу (!). А к указателю на слайс или мэпу нельзя (это можно объяснить но может быть нелегко запомнить).
Также len(...) нормально проглатывает nil-ы если они имеют один из вышеуказанных типов.
Можно годами жить и не знать про неё. Она возвращает капасити слайса. Кроме слайса можно её вызвать на массиве, хотя смысла в этом нет. Когда она нужна? не думаю что вы легко придумаете хорошие кейсы :)
Для самоконтроля, проверьте, чему равны капасити слайсов в коде ниже:
a := []int{2, 3, 5, 7, 9}
println(cap(a))
b := a[1:4]
println(cap(b))
Наверное один из первых вопросов - что можно сделать с её помощью (слайс, мэпу или канал). При этом вторым аргументом идёт размер - для слайса обязательный, для мэпы и канала опциональный. Для мэпы он определяет начальное количество "бакетов", но поскольку она растёт автоматически, об этом нечасто вспоминают. Для слайсов можно указать и третий аргумент - капасити (т.е. размер безымянного массива под данным слайсом).
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 может модифицировать нижележащий массив если есть капасити. Нечасто на это наткнёшься, но особенно при передаче в функцию - можно:
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 аргументы на самом деле передаются с помощью слайса - то есть это "синтаксический сахар", а не хардкорная реализация как в С.
Ещё одна функция о которой спокойно можно не знать. И может даже лучше не знать. Она очищает мэпы и слайсы. А к массиву её применить нельзя (логика?) - хотя можно если покастить в слайс той же длины (см ниже).
Причем мэпа просто становится пустой, а в слайсе проставляются "нулевые" значения данного типа, что может быть слегка неочевидно.
Из той же оперы: функция, копирующая слайс в слайс. Почему бы не сделать функцию "клонирующую" слайс - непонятно. Из нюансов - она умеет копировать строку в слайс байт. Также отдельно подчёркивается что слайсы могут пересекаться но не сказано, какого результата мы при этом ожидаем. Видимо у авторов реминисценции по поводу strcpy(...) из языка С, которая при неаккуратном использовании в этом случае могла привести к затиранию признака конца строки с последующим segfault или порчей других переменных.
a := []int{2, 3, 5, 7, 11}
copy(a[0:2], a[2:4])
fmt.Printf("%v\n", a)
Можете попробовать угадывать какой массив получается в результате (а что если переставить аргументы функции местами?) - хотя если я правильно понимаю, раз не указано, то результат может зависеть даже от версии.
Встроенные функции появившиеся кажется с 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[:] // вот так норм
Из этого вопроса следует ещё один, идеологический и абстрактный
Имеется в виду - использовать только слайсы под которыми выделены неявные массивы. Я на этот вопрос хмыкнул и ответил утвердительно, поскольку не припомню (и не придумаю случая) когда нужен строго массив или когда слайс будет мешать. Интервьюер тоже хмыкнул и сказал что-то неопределенное вроде "угу" - вероятно сам тоже не мог ни припомнить ни придумать. Зачем спрашивал? чисто "посмотреть как собеседник рассуждает"?
Пожалуй на этом остановимся - всем успехов! Смело добавляйте, поправляйте, критикуйте!