Как стать автором
Обновить

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

Уровень сложностиПростой
Время на прочтение6 мин
Количество просмотров3.4K

Набив несколько шишек поначалу мы начинаем довольно уверенно пользоваться массивами и слайсами в 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 без массивов вообще

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

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

Теги:
Хабы:
Всего голосов 9: ↑8 и ↓1+12
Комментарии16

Публикации

Истории

Работа

Go разработчик
94 вакансии

Ближайшие события

2 – 18 декабря
Yandex DataLens Festival 2024
МоскваОнлайн
11 – 13 декабря
Международная конференция по AI/ML «AI Journey»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань