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

76bb6dbffb72a914c3d65e5ae6ca49eb.png

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

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

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

© Habrahabr.ru