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

Может ли функция возвращать массив (а не слайс)

Конечно может - хотя на практике это увидишь нечасто (может поэтому и пытаются "подловить"?) Случаи когда нужно вернуть данные фиксированного размера встречаются например при подсчете хэшей. Как пример - в 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 без массивов вообще

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

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

Комментарии (6)


  1. Rezzet
    05.10.2024 07:58

    У меня только один вопрос - зачем это вообще нужно? Выглядит как добавление лишней сущности.


  1. gudvinr
    05.10.2024 07:58

    Можно ли было оставить Go без массивов вообще

    Массивы в го - бесплатные. Т.е. на них не тратятся дополнительные ресурсы.

    Для слайсов в любом случае пришлось бы выделять кусок памяти и хранить на него указатель. Массив - это и есть просто сырой кусок памяти.

    Каждый слайс - это +16 байт. Это может мешать, если у вас очень много мелких сущностей - это конечно случай граничный, но не нереалистичный. Пример - IP адреса, хеши, да и вообще всё у чего не меняется размер. Для IPv4 заголовок слайса занимает большую часть хранилища.

    Не совсем очевидный момент, но массивы лучше переживают escape to heap. По крайней мере раньше можно было выделить массив большего размера, прежде чем он вытесняется в кучу.


  1. QtRoS
    05.10.2024 07:58
    +2

    Стоит упомянуть про то, что массивы по семантике ближе к значениям, а слайсы к ссылкам. На практике это заметно в кейсах:


    1. RodionGork Автор
      05.10.2024 07:58

      Здорово, спасибо! Честно скажу - абсолютно не обращал на это внимания сам - ну как во вступлении и сказано :)


  1. lazy_val
    05.10.2024 07:58
    +2

    итак, размер удваивается

    Вообще-то удваивается до определенного предела 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


  1. coctic
    05.10.2024 07:58

    Про необходимость массивов.

    Если читать какой-нибудь бинарный протокол при помощи binary.Read, то структура с членом-массивом читается без проблем, а если бы это были слайсы - как минимум перед чтением надо было проинициализировать размеры этих слайсов или как-то по-другому передать размеры. Можно, но с массивом гораздо удобнее и ошибкоустойчивее.