Половина задач «что выведет» на Go-собесе про слайсы. Чтобы отвечать уверенно, достаточно одной картинки: слайс это маленькая структура из трёх полей.
Заголовок слайса
type slice struct {
array unsafe.Pointer // указатель на базовый массив
len int
cap int
}
Слайс передаётся в функцию по значению, то есть копируется этот заголовок. Копия смотрит в тот же массив. Поэтому функция может поменять элементы, но не может поменять длину слайса вызывающего.
func set(s []int) { s[0] = 100 } // видно снаружи
func add(s []int) { s = append(s, 1) } // снаружи len не изменится
Срезы делят массив
a := []int{1, 2, 3, 4, 5}
b := a[1:3] // [2 3], len 2, cap 4
b[0] = 20 // a = [1 20 3 4 5]
b = append(b, 30) // места хватает: a = [1 20 3 30 5]
append пишет в общий массив, если len < cap. Если места нет, он выделяет новый массив, копирует данные, и дальше слайсы живут отдельно. Как раз отсюда «то меняется, то нет».
Ограничить ёмкость помогает полное выражение среза a[low:high:max]: b := a[1:3:3] даёт cap 2, и следующий append гарантированно скопирует данные.
Рост ёмкости
Для маленьких слайсов ёмкость при нехватке удваивается, для больших (после порога около 256 элементов) растёт плавнее, примерно на четверть плюс константа. Точные числа зависят от версии рантайма и округления под классы размеров аллокатора, поэтому полагаться на них нельзя. Если размер известен, выделяйте заранее: make([]T, 0, n).
nil-слайс и пустой слайс
var a []int // nil, len 0
b := []int{} // не nil, len 0
Оба работают с len, range и append. Разница видна в a == nil и в JSON: nil-слайс кодируется как null, пустой как [].
copy, удаление, утечки
copy(dst, src) копирует min(len(dst), len(src)) элементов. Удалить элемент i с сохранением порядка: s = append(s[:i], s[i+1:]...) или slices.Delete(s, i, i+1) (Go 1.21+). Помните, что маленький срез большого массива держит в памяти весь массив. Нужен кусочек надолго: скопируйте его (slices.Clone).
Массивы
Массив [4]int это значение: присваивание и передача в функцию копируют все элементы. Длина входит в тип, [3]int и [4]int разные типы.
Мапы
Мапа это указатель на хеш-таблицу рантайма, поэтому передача в функцию дёшева, а изменения видны снаружи.
- Порядок обхода
rangeнамеренно случайный. Нужен порядок: соберите ключи и отсортируйте (slices.Sorted(maps.Keys(m))). - Чтение отсутствующего ключа даёт нулевое значение. Отличить «нет ключа» от «ноль» помогает
v, ok := m[k]. - Запись в nil-мапу вызывает panic.
- Удалять ключи во время
rangeбезопасно. Добавленные во время обхода ключи могут как попасть в обход, так и нет. - Элемент мапы не адресуем:
m[k].Field = 1не компилируется дляmap[K]Struct. Нужно достать, поменять, положить обратно, или хранить указатели. - Мапа не безопасна для конкурентной записи: рантайм часто замечает такую гонку и аварийно завершает программу с
fatal error: concurrent map writes.
Внутри до Go 1.23 мапа была массивом бакетов по 8 элементов с цепочками переполнения и постепенной эвакуацией при росте. С Go 1.24 реализация переписана на Swiss tables: группы по 8 слотов с байтами-метками и поиском сразу по группе. Снаружи поведение то же.
Частые ошибки
- Ожидать, что
appendвнутри функции изменит длину слайса снаружи. - Брать срез и делать
append, затирая данные исходного слайса. - Полагаться на порядок обхода мапы в тестах.
- Писать в мапу из нескольких горутин без мьютекса.
Что спросят на собесе
- Из чего состоит слайс и что происходит при
append. - Почему изменение в одном слайсе видно в другом и как этого избежать.
- Чем nil-слайс отличается от пустого.
- Как устроена мапа, почему порядок обхода случайный, что изменилось в Go 1.24.
- Почему нельзя взять адрес элемента мапы и писать в мапу конкурентно.