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создаёт изменяемое представление.
Типовые вопросы
- Почему после
appendнадо присваивать результат?- Длина среза меняется в возвращённом дескрипторе; исходный срез не становится длиннее автоматически.
- Копирует ли
t := sэлементы?- Нет, копируется заголовок, поэтому
tиsобычно разделяют backing array.
- Нет, копируется заголовок, поэтому
- Что вернёт чтение отсутствующего ключа map?
- Zero value типа значения; чтобы отличить отсутствие от сохранённого нуля, используют форму с
ok.
- Zero value типа значения; чтобы отличить отсутствие от сохранённого нуля, используют форму с
- Почему нельзя одновременно читать и писать в map?
- Такая работа без синхронизации — data race и может привести к runtime fatal error; доступ надо координировать.
- Как посчитать символы Unicode в строке?
utf8.RuneCountInStringилиlen([]rune(s))при необходимости рун; индексирование строки даёт байты.
- Что делает
s[i:j:j]?- Возвращает срез длины
j-iи capacityj-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 · Указатели и семантика значений