Срезы, массивы и map в Go
Почти любая программа работает не с одним значением, а с набором значений: список файлов, строки лога, пользователи в чате, пароли в менеджере, задачи для worker pool, результаты HTTP-запросов. В Go основные коллекции - это массивы, срезы (slice) и словари (map).
Если объяснить коротко:
- массив - коробка фиксированного размера;
- срез - гибкое окно поверх массива;
- map - таблица, где значение ищется по ключу.
Но для реального кода этого мало. Нужно понимать, как работает append, почему срезы иногда меняют общий массив, почему порядок map случайный, зачем нужен copy, и почему map нельзя писать из нескольких горутин без защиты.
Массив
Массив имеет фиксированную длину, и длина является частью типа:
var a [3]int a[0] = 10 a[1] = 20 a[2] = 30
Тип [3]int и тип [4]int - разные типы.
var three [3]int var four [4]int // three = four // ошибка компиляции
Массивы полезны, когда размер действительно фиксирован: например, 16 байт UUID, 32 байта SHA-256, координаты из двух чисел. Но в обычных списках чаще используют срезы.
Срез
Срез выглядит как динамический массив:
numbers := []int{10, 20, 30} numbers = append(numbers, 40)
Но внутри срез - это маленькая структура:
slice ├── pointer -> внутренний массив ├── len -> текущая длина └── cap -> емкость
len показывает, сколько элементов доступно сейчас. cap показывает, сколько элементов можно хранить в текущем внутреннем массиве до перевыделения.
items := make([]string, 0, 3) fmt.Println(len(items)) // 0 fmt.Println(cap(items)) // 3
append
append добавляет элементы и возвращает новый срез:
items = append(items, "a") items = append(items, "b")
Почему нужно присваивать результат?
append(items, "c") // ошибка: результат append не использован
Когда емкости хватает, append записывает в тот же внутренний массив. Когда емкости не хватает, Go создает новый массив, копирует старые элементы и возвращает срез на новый массив.
Срезы разделяют память
numbers := []int{1, 2, 3, 4} part := numbers[1:3] part[0] = 99 fmt.Println(numbers) // [1 99 3 4]
part не копирует данные. Он смотрит на тот же внутренний массив. Это эффективно, но может удивлять.
Если нужна независимая копия:
copyOfPart := append([]int(nil), part...)
Или через copy:
copyOfPart := make([]int, len(part)) copy(copyOfPart, part)
copy
Функция copy(dst, src) копирует элементы из src в dst:
src := []string{"a", "b", "c"} dst := make([]string, len(src)) copy(dst, src)
Важно: copy копирует максимум min(len(dst), len(src)) элементов.
src := []int{1, 2, 3} dst := make([]int, 2) copy(dst, src) fmt.Println(dst) // [1 2]
range
range перебирает коллекцию:
for index, value := range numbers { fmt.Println(index, value) }
Если индекс не нужен:
for _, value := range numbers { fmt.Println(value) }
Если нужно только количество итераций:
for i := range numbers { fmt.Println(i) }
map
map хранит значения по ключу:
ages := map[string]int{ "Alice": 25, "Bob": 31, } fmt.Println(ages["Alice"])
Создание пустой map:
counts := make(map[string]int) counts["error"]++
Если читать отсутствующий ключ, Go вернет нулевое значение типа:
fmt.Println(counts["missing"]) // 0
Чтобы отличить "ключа нет" от "ключ есть, но значение 0", используйте ok:
value, ok := counts["missing"] if !ok { fmt.Println("ключа нет") }
Удаление из map
delete(counts, "error")
Если ключа нет, delete ничего не делает. Это удобно: не нужно проверять наличие заранее.
map как set
В Go нет отдельного встроенного set, но его легко сделать через map:
seen := make(map[string]bool) seen["alice@example.com"] = true if seen["alice@example.com"] { fmt.Println("уже видели") }
Более экономный вариант:
seen := make(map[string]struct{}) seen[id] = struct{}{} if _, ok := seen[id]; ok { fmt.Println("есть") }
struct{}{} не занимает полезной памяти, поэтому такой set часто используют для дедупликации.
Порядок обхода map
Порядок range по map не гарантируется:
for key, value := range counts { fmt.Println(key, value) }
При разных запусках порядок может быть разным. Если нужен стабильный вывод, отсортируйте ключи:
keys := make([]string, 0, len(counts)) for key := range counts { keys = append(keys, key) } sort.Strings(keys) for _, key := range keys { fmt.Println(key, counts[key]) }
Это важно для отчетов, тестов, README-примеров и JSON-снимков.
Сортировка срезов
Для базовых типов:
sort.Ints(numbers) sort.Strings(names)
Для структур:
type User struct { Name string Age int } sort.Slice(users, func(i, j int) bool { return users[i].Age < users[j].Age })
sort.Slice меняет срез на месте. Если нужно сохранить оригинал:
sorted := append([]User(nil), users...) sort.Slice(sorted, func(i, j int) bool { return sorted[i].Age < sorted[j].Age })
make и new
make создает и инициализирует slice, map или channel:
items := make([]string, 0, 10) counts := make(map[string]int) jobs := make(chan Job)
new выделяет память под значение и возвращает указатель:
number := new(int) *number = 10
В обычном коде с коллекциями почти всегда нужен make, а не new.
nil slice и nil map
nil slice можно читать и добавлять через append:
var items []string items = append(items, "a")
nil map читать можно, но писать нельзя:
var counts map[string]int fmt.Println(counts["x"]) // 0 // counts["x"] = 1 // panic
Перед записью map нужно создать:
counts = make(map[string]int) counts["x"] = 1
Байтовые срезы
[]byte - это срез байтов. Он часто встречается в файлах, сетевых ответах, JSON, шифровании и архивах.
data := []byte("hello") fmt.Println(data) // [104 101 108 108 111] fmt.Println(string(data)) // hello
Строка (string) неизменяема. Срез байтов ([]byte) можно менять.
Частые ошибки
- Ожидать, что
appendменяет срез без присваивания. - Забыть, что два среза могут смотреть на один массив.
- Писать в
nilmap. - Ожидать стабильного порядка обхода map.
- Сортировать оригинальный срез, когда нужна копия.
- Использовать map из нескольких горутин без
sync.Mutex. - Считать, что
lenиcap- одно и то же.
Практический пример: счетчик расширений файлов
func CountExtensions(files []string) map[string]int { counts := make(map[string]int) for _, file := range files { ext := filepath.Ext(file) if ext == "" { ext = "<no-ext>" } counts[ext]++ } return counts }
Стабильный вывод:
counts := CountExtensions(files) extensions := make([]string, 0, len(counts)) for ext := range counts { extensions = append(extensions, ext) } sort.Strings(extensions) for _, ext := range extensions { fmt.Printf("%s: %d\n", ext, counts[ext]) }
Этот паттерн встречается во многих задачах: сначала собираем статистику в map, потом сортируем ключи для красивого вывода.
Шпаргалка
// slice items := []string{"a", "b"} items = append(items, "c") copyItems := append([]string(nil), items...) // map counts := make(map[string]int) counts["ok"]++ value, ok := counts["ok"] delete(counts, "ok") // set seen := make(map[string]struct{}) seen[id] = struct{}{} // stable map output keys := make([]string, 0, len(counts)) for key := range counts { keys = append(keys, key) } sort.Strings(keys)