Содержание

Срезы, массивы и 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) можно менять.

Частые ошибки

  1. Ожидать, что append меняет срез без присваивания.
  2. Забыть, что два среза могут смотреть на один массив.
  3. Писать в nil map.
  4. Ожидать стабильного порядка обхода map.
  5. Сортировать оригинальный срез, когда нужна копия.
  6. Использовать map из нескольких горутин без sync.Mutex.
  7. Считать, что 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)

Следующий шаг после статьи

Закрепите тему во вводном проекте без регистрации, а затем переходите к курсам.

Продолжить изучение

Выбери следующую статью по маршруту или углубись в смежную тему.

Похожие статьи