Go: массивы, срезы, map и строки

Массив [N]T — значение фиксированной длины; длина входит в его тип. Срез []T — маленький дескриптор окна над backing array: указатель, длина и capacity. map[K]V — хеш-таблица с ключами сопоставимого типа. Строка — неизменяемая последовательность байтов, обычно UTF-8, но язык не требует валидной UTF-8.

Зачем это на интервью

Большинство неприятных Go-багов в прикладном коде — это aliasing срезов, неожиданный append, удержание большого массива маленьким под-срезом, конкурентный доступ к map и путаница между байтами и рунами. На интервью важнее объяснить представление и последствия, чем помнить эвристику роста capacity.

Минимум для E4

  • Отличать массив как копируемое значение от среза как дескриптора разделяемого массива.
  • Называть len и cap среза, понимать полное выражение s[low:high:max].
  • Создавать map через make или литерал и проверять наличие ключа через v, ok := m[k].
  • Объяснять, почему len("€") == 3, а число рун равно одной.
package main
 
import "fmt"
 
func main() {
	s := make([]int, 2, 4)
	s[0], s[1] = 10, 20
	view := s[:1]
	view = append(view, 99) // capacity достаточна: меняет тот же backing array
 
	m := map[string]int{"go": 1}
	v, ok := m["rust"]
	fmt.Println(s, view, v, ok, len("€"), []rune("€"))
}

append возвращает новый срез; всегда сохраняйте результат. Он может использовать старый массив либо выделить новый — это деталь реализации, на которую нельзя опираться для корректности.

Углубление для E5/Senior

Срезы должны иметь явную модель владения. Если функция только читает []byte, можно передать срез без копии и задокументировать срок жизни. Если она сохраняет, изменяет или передаёт его в асинхронную работу, вызывающая сторона должна знать об aliasing; часто безопаснее bytes.Clone, slices.Clone или копирование через append([]T(nil), s...). Под-срез big[:10] держит весь backing array живым: для долгоживущего результата скопируйте нужную часть.

Трёхиндексный срез ограничивает capacity и предотвращает запись append в хвост общего массива: part := src[i:j:j]. Это не копия, но следующий append вынужден выделить новый массив. Нельзя обещать конкретный коэффициент роста capacity: runtime может менять его между версиями; планирование capacity — оптимизация, подтверждаемая benchmark-ом и профилем.

Обычная map небезопасна при одновременных чтениях и записях. Защита выбирается по нагрузке: sync.Mutex, sync.RWMutex, владение одной goroutine, copy-on-write либо sync.Map для его специализированных паттернов. Итерация map намеренно не имеет стабильного порядка; для детерминированного ответа собирают и сортируют ключи.

Ключевые понятия

  • Array — [N]T; размер — часть типа, присваивание копирует все элементы.
  • Slice header — дескриптор backing array с len и cap; копируется сам дескриптор, не элементы.
  • Aliasing — два среза видят один массив и могут наблюдать изменения друг друга.
  • append — добавляет элементы и возвращает новый заголовок; может выделить массив.
  • Map lookup с ok — отличает отсутствующий ключ от сохранённого zero value.
  • Rune — кодовая точка Unicode (int32); byte — один октет.
  • Immutable string — байты строки нельзя менять по индексу; преобразование в []byte создаёт изменяемое представление.

Типовые вопросы

  1. Почему после append надо присваивать результат?
    • Длина среза меняется в возвращённом дескрипторе; исходный срез не становится длиннее автоматически.
  2. Копирует ли t := s элементы?
    • Нет, копируется заголовок, поэтому t и s обычно разделяют backing array.
  3. Что вернёт чтение отсутствующего ключа map?
    • Zero value типа значения; чтобы отличить отсутствие от сохранённого нуля, используют форму с ok.
  4. Почему нельзя одновременно читать и писать в map?
    • Такая работа без синхронизации — data race и может привести к runtime fatal error; доступ надо координировать.
  5. Как посчитать символы Unicode в строке?
    • utf8.RuneCountInString или len([]rune(s)) при необходимости рун; индексирование строки даёт байты.
  6. Что делает s[i:j:j]?
    • Возвращает срез длины j-i и capacity j-i, исключая повторное использование хвоста исходного массива через append.

Практика

  • Реализуйте CloneAndAppend без изменения входного среза. Готово: тест меняет исходный и результат в обе стороны и доказывает отсутствие aliasing.
  • Напишите частотный словарь слов. Готово: отсутствующий ключ обработан корректно, результат выдаётся в отсортированном порядке, go test -race проходит.
  • Реализуйте разворот Unicode-строки по рунам. Готово: тесты включают ASCII, €, комбинируемые символы с оговоркой о grapheme clusters и невалидную UTF-8.

Частые ошибки и ловушки

  • Использовать &v из for range как адрес элемента: переменная цикла не является элементом коллекции.
  • Сохранять малый под-срез от огромного буфера и удерживать память.
  • Полагаться на порядок range по map.
  • Записывать в nil map или читать/писать map из нескольких goroutine без синхронизации.
  • Называть rune «видимым символом»: один пользовательский grapheme cluster может состоять из нескольких рун.

Связанные темы

Go · Основы языка Go · Типы, константы и iota · Указатели и семантика значений

Источники