Почти любая программа хранит списки и таблицы соответствий: список заказов, словарь «код страны → название». В Go для этого есть два рабочих инструмента — slice (срез) и map. Они выглядят просто, но у срезов есть пара особенностей, на которых спотыкаются почти все новички. Разберём с самого начала.
Массив: фиксированный размер
Сначала про массив (array) — потому что срез построен на нём. Массив в Go — это последовательность элементов фиксированной длины, и длина — часть типа.
var a [3]int // массив из 3 int, по умолчанию [0 0 0]
a[0] = 10
fmt.Println(a) // [10 0 0]
fmt.Println(len(a)) // 3
[3]int и [4]int — это разные типы. Нельзя присвоить один другому, нельзя передать [4]int в функцию, которая ждёт [3]int. Из-за этой жёсткости массивы в обычном коде встречаются редко. Чаще нужен список, который умеет расти, — и тут на сцену выходит срез.
Срез (slice): окно в массив
Срез — это гибкий, изменяемый по размеру взгляд на последовательность элементов. Объявляется как тип без числа в скобках: []int, []string.
s := []int{10, 20, 30} // литерал среза
fmt.Println(s) // [10 20 30]
fmt.Println(len(s)) // 3
Важно понять, что срез внутри — это не сами данные, а маленькая структура из трёх полей:
- указатель на нижний массив (где реально лежат элементы),
- len — длина (сколько элементов сейчас видно),
- cap — ёмкость (сколько элементов помещается от начала среза до конца нижнего массива).
s := make([]int, 2, 5) // длина 2, ёмкость 5
fmt.Println(s) // [0 0]
fmt.Println(len(s)) // 2
fmt.Println(cap(s)) // 5
Функция make([]T, len, cap) создаёт срез с заранее выделенной ёмкостью. Третий аргумент cap необязателен; если его опустить, ёмкость равна длине.
Короткая формула: срез — это «окно» (указатель + len + cap) поверх массива в памяти.
append: как срез растёт
Добавляют элементы встроенной функцией append. Она возвращает срез — и результат обязательно надо присвоить обратно:
s := []int{1, 2}
s = append(s, 3) // [1 2 3]
s = append(s, 4, 5) // [1 2 3 4 5] — можно несколько сразу
Что происходит внутри. Если в нижнем массиве ещё есть запас (len < cap), append просто записывает элемент в свободную ячейку и увеличивает len. Если запаса нет (len == cap), Go выделяет новый, больший массив, копирует туда старые элементы, дописывает новый — и возвращает срез, указывающий уже на новый массив.
Поэтому забыть s = ... — частая ошибка: без присваивания вы потеряете и новый элемент, и (после переаллокации) вообще ничего не добавите к видимому срезу.
Если заранее известно, сколько элементов будет, выделите ёмкость сразу — меньше переаллокаций:
ids := make([]int, 0, 100) // длина 0, но место под 100 уже есть
for i := 0; i < 100; i++ {
ids = append(ids, i) // ни одной переаллокации
}
Нарезка: s[low:high]
Из среза (или массива) можно вырезать под-срез синтаксисом s[low:high]. Берутся элементы с индекса low включительно до high не включительно.
s := []int{10, 20, 30, 40, 50}
fmt.Println(s[1:3]) // [20 30]
fmt.Println(s[:2]) // [10 20] — low по умолчанию 0
fmt.Println(s[2:]) // [30 40 50] — high по умолчанию len
И вот ключевой момент, ведущий к самой частой грабле.
Грабля: срезы делят общий нижний массив
Нарезка не копирует данные. Под-срез указывает на тот же самый массив, что и оригинал. Значит, запись через один срез видна через другой:
s := []int{10, 20, 30, 40}
part := s[1:3] // [20 30], но это окно в тот же массив
part[0] = 999
fmt.Println(s) // [10 999 30 40] — изменился и оригинал!
То же самое коварно проявляется с append. Если у под-среза есть запас ёмкости, append пишет в общий массив и затирает соседние элементы оригинала:
s := []int{10, 20, 30, 40}
part := s[0:2] // len 2, но cap 4 — запас есть
part = append(part, 999) // пишет в s[2], не выделяя новый массив
fmt.Println(s) // [10 20 999 40] — оригинал испорчен
Когда нужна независимая копия — не нарезайте, а копируйте через copy:
src := []int{10, 20, 30}
dst := make([]int, len(src))
copy(dst, src) // dst — отдельный массив
dst[0] = 999
fmt.Println(src) // [10 20 30] — не затронут
Короткая формула: нарезка делит память, copy — разделяет её.
map: таблица соответствий
map — это словарь «ключ → значение» (в других языках hash map, dictionary). Тип записывается как map[KeyType]ValueType.
m := map[string]int{
"alice": 30,
"bob": 25,
}
m["carol"] = 28 // добавить или перезаписать
fmt.Println(m["bob"]) // 25
Создать пустую map можно через make или литералом map[string]int{}. Просто var m map[string]int даёт nil-map — из неё можно читать, но запись в неё вызовет панику (об этом ниже).
Чтение с проверкой: «запятая ok»
Если запросить отсутствующий ключ, map вернёт нулевое значение типа, а не ошибку:
m := map[string]int{"alice": 30}
fmt.Println(m["zzz"]) // 0 — но был ли такой ключ?
Чтобы отличить «лежит ноль» от «ключа нет», используют форму с двумя значениями — её называют «запятая ok»:
v, ok := m["zzz"]
if ok {
fmt.Println("нашли:", v)
} else {
fmt.Println("ключа нет") // сюда
}
Удаление
delete(m, "alice") // удалить ключ; если ключа нет — ничего не делает
range: перебор срезов и map
Цикл for ... range перебирает оба типа. Для среза он даёт индекс и значение:
s := []string{"a", "b", "c"}
for i, v := range s {
fmt.Println(i, v) // 0 a / 1 b / 2 c
}
Если индекс не нужен — замените его на _:
for _, v := range s {
fmt.Println(v)
}
Для map range даёт ключ и значение:
m := map[string]int{"alice": 30, "bob": 25}
for key, value := range m {
fmt.Println(key, value)
}
Важно: порядок обхода map не определён и меняется от запуска к запуску — Go делает это намеренно. Если нужен стабильный порядок, соберите ключи в срез и отсортируйте его отдельно.
Нулевые значения: nil-срез и nil-map
У необъявленных через make/литерал срезов и map нулевое значение — nil. Ведут они себя по-разному, и это стоит запомнить.
nil-срез полностью рабочий для чтения и для append:
var s []int // nil
fmt.Println(s == nil) // true
fmt.Println(len(s)) // 0
s = append(s, 1) // работает! append сам выделит массив
Поэтому отдельно инициализировать срез перед append обычно не нужно — можно начинать прямо с var s []T.
nil-map можно читать, но нельзя писать — запись паникует:
var m map[string]int // nil
fmt.Println(m["x"]) // 0 — читать можно
m["x"] = 1 // panic: assignment to entry in nil map
Поэтому map перед записью всегда создавайте через make или литерал:
m := make(map[string]int) // теперь можно писать
m["x"] = 1
Короткая формула: nil-срез писать можно (через append), nil-map — нельзя.
Коротко
- Массив — фиксированной длины, длина — часть типа; в обычном коде редок.
- Срез — это указатель + len + cap поверх массива; гибкий и растущий.
appendвозвращает срез — всегда присваивайте результат обратно; при нехватке cap он выделяет новый массив.- Нарезка
s[low:high]не копирует — под-срез делит память с оригиналом, запись иappendмогут затронуть соседние данные. Нужна независимость —copy. - map — словарь «ключ → значение»; читайте через «запятая ok», чтобы отличить отсутствие ключа от нулевого значения; удаляйте через
delete. rangeдаёт индекс+значение для среза и ключ+значение для map; порядок обхода map не определён.- nil-срез годен для чтения и
append; в nil-map писать нельзя — создавайте черезmake.
Что почитать дальше
- Синтаксис и типы — переменные, базовые типы и нулевые значения, на которых всё это стоит.
- Структуры и методы — как из срезов и map собирать составные данные.
- Указатели — почему срез передаётся «по ссылке на массив», а массив копируется целиком.