Лекция 6. Maps, строки, байты и Unicode
Цели лекции
После лекции студент должен:
- создавать и изменять maps;
- проверять наличие ключа через форму
value, ok; - понимать поведение отсутствующего ключа и
nilmap; - использовать map как множество и частотный словарь;
- не зависеть от порядка обхода map;
- понимать, что строка Go содержит байты;
- различать
string,byte,rune,[]byteи[]rune; - корректно обрабатывать UTF-8;
- применять пакеты
strings,unicode,unicode/utf8; - избегать лишних конкатенаций и некорректной индексации текста.
1. Ассоциативные данные
Slice хранит значения по целочисленным индексам. Во многих задачах доступ нужен по смысловому ключу:
- оценка по имени студента;
- пользователь по идентификатору;
- количество слов по самому слову;
- настройка по её названию.
Для этого в Go используется map.
scores := map[string]int{ "Анна": 95, "Борис": 82,}Тип читается как «map из string в int».
2. Тип ключа и значения
Общая форма:
map[KeyType]ValueTypeКлюч должен быть сравнимым типом. Подходят:
- строки;
- числа;
- логические значения;
- указатели;
- массивы и структуры, если все их элементы сравнимы.
Slices, maps и функции нельзя использовать как ключи, потому что они не
сравниваются через ==.
Допустимые примеры:
map[int]stringmap[string]float64map[UserID]Usermap[[2]int]stringЗначением может быть практически любой тип, включая slice или другую map.
3. Создание map литералом
capitals := map[string]string{ "Россия": "Москва", "Франция": "Париж", "Япония": "Токио",}Пустая, но готовая к записи map:
capitals := map[string]string{}4. Создание через make
scores := make(map[string]int)scores["Анна"] = 95scores["Борис"] = 82Можно подсказать ожидаемое число элементов:
scores := make(map[string]int, 100)Это не ограничение размера, а подсказка для уменьшения перераспределений. Map может вырасти больше указанного числа.
5. Чтение и запись
scores := map[string]int{ "Анна": 95,}
fmt.Println(scores["Анна"])
scores["Анна"] = 97scores["Борис"] = 82Одна форма присваивания и добавляет новый ключ, и изменяет существующий.
Количество элементов:
fmt.Println(len(scores))Вместимость map через cap получить нельзя.
6. Отсутствующий ключ и zero value
Чтение отсутствующего ключа возвращает zero value типа значения:
scores := map[string]int{ "Анна": 95,}
fmt.Println(scores["Виктор"]) // 0Но нулевой балл может быть реальным значением. Чтобы отличить отсутствие ключа,
используют дополнительный bool:
score, ok := scores["Виктор"]if !ok { fmt.Println("Студент не найден") return}
fmt.Println("Балл:", score)Эту форму называют comma-ok idiom.
Короткая инструкция:
if score, ok := scores["Анна"]; ok { fmt.Println("Балл:", score)}7. Удаление и очистка
Удаление ключа:
delete(scores, "Анна")Удаление отсутствующего ключа безопасно и ничего не делает.
Очистка всех элементов:
clear(scores)После clear сама map остаётся готовой к повторной записи.
Можно также присвоить новую map:
scores = make(map[string]int)Старая map будет освобождена сборщиком мусора, когда на неё не останется ссылок.
8. nil map
Zero value map — nil:
var scores map[string]intЧитать из неё можно:
fmt.Println(scores["Анна"]) // 0fmt.Println(len(scores)) // 0Удаление и clear также безопасны. Но запись вызывает panic:
// scores["Анна"] = 95 // panic: assignment to entry in nil mapПеред записью map нужно создать:
scores = make(map[string]int)scores["Анна"] = 95Функция, которая только читает map, может корректно принимать nil. Функция,
которая записывает, должна получить созданную map или создать и вернуть её.
9. Перебор map
for name, score := range scores { fmt.Println(name, score)}Порядок обхода map не определён. Он может различаться между запусками. Нельзя:
- сравнивать текстовый вывод, рассчитывая на порядок;
- выбирать «первый» элемент как особенный;
- использовать порядок в бизнес-логике.
Для стабильного вывода сортируют ключи:
names := make([]string, 0, len(scores))for name := range scores { names = append(names, name)}
slices.Sort(names)
for _, name := range names { fmt.Println(name, scores[name])}10. Map присваивается как ссылка на общие данные
first := map[string]int{"one": 1}second := first
second["two"] = 2
fmt.Println(first) // содержит one и twoПрисваивание не создаёт независимую копию. Для клонирования можно использовать
пакет maps:
clone := maps.Clone(first)Клонирование поверхностное. Если значениями являются slices или maps, вложенные данные остаются общими.
Maps нельзя сравнивать через ==, кроме сравнения с nil.
11. Передача map в функцию
func increment(counter map[string]int, key string) { counter[key]++}
func main() { counter := make(map[string]int) increment(counter, "go") fmt.Println(counter["go"]) // 1}Функция получает копию небольшого описателя map, но обе копии обращаются к одной внутренней структуре.
Если функция может заменить map целиком, новую map следует вернуть:
func reset() map[string]int { return make(map[string]int)}
counter = reset()12. Частотный словарь
Map удобно считает повторения:
words := []string{"go", "python", "go", "swift", "go"}frequency := make(map[string]int)
for _, word := range words { frequency[word]++}
fmt.Println(frequency["go"]) // 3Отсутствующий ключ возвращает 0, поэтому отдельная проверка перед инкрементом
не нужна.
13. Map как множество
В стандартной библиотеке нет отдельного типа set. Множество часто представляют
как map[T]struct{}:
languages := map[string]struct{}{ "go": {}, "python": {},}Добавление:
languages["swift"] = struct{}{}Краткая форма:
languages["swift"] = struct{}{}Проверка:
_, exists := languages["go"]Удаление:
delete(languages, "swift")struct{} не содержит полей и не требует места для полезных данных. Иногда
используют map[T]bool, но тогда false и отсутствие ключа требуют внимания.
14. Вложенные maps и slices
Map со slice значений:
courses := map[string][]string{ "backend": {"Go", "PostgreSQL", "Docker"}, "mobile": {"Swift", "SwiftUI"},}Добавление:
courses["backend"] = append(courses["backend"], "Redis")Вложенная map требует создания каждого уровня:
grades := make(map[string]map[string]int)grades["Анна"] = make(map[string]int)grades["Анна"]["Go"] = 95Попытка записать во внутреннюю nil map вызовет panic.
Часто структура данных понятнее вложенных maps. Структуры рассматриваются в следующей лекции.
15. Строка как последовательность байтов
Строка Go — неизменяемая последовательность байтов:
text := "Go"fmt.Println(len(text)) // 2fmt.Println(text[0]) // 71 — байт символа 'G'Индексирование строки возвращает byte, а не строку и не обязательно целый
Unicode-символ.
text := "Мир"fmt.Println(len(text)) // 6 байт в UTF-8fmt.Println(text[0]) // только первый байт буквы 'М'Нельзя заменить отдельный байт строки:
// text[0] = 'м' // ошибка: строки неизменяемы16. byte и rune
Псевдонимы:
type byte = uint8type rune = int32byte хранит один байт. rune хранит Unicode code point.
var latin rune = 'G'var cyrillic rune = 'Ж'
fmt.Printf("%c %U\n", latin, latin)fmt.Printf("%c %U\n", cyrillic, cyrillic)Один Unicode-символ кодируется в UTF-8 одним–четырьмя байтами.
Важно: rune соответствует code point, но не всегда тому, что пользователь воспринимает как один видимый символ. Например, буква и комбинируемый знак могут состоять из нескольких runes, а emoji — из последовательности code points.
17. Перебор строки через range
text := "Go, мир!"
for index, symbol := range text { fmt.Printf("index=%d rune=%c code=%U\n", index, symbol, symbol)}index — байтовое смещение начала символа. Для ASCII он увеличивается на 1, для
кириллицы обычно на 2.
Если нужны только runes:
for _, symbol := range text { fmt.Printf("%c ", symbol)}18. Длина строки
Число байтов:
len(text)Число UTF-8 code points:
utf8.RuneCountInString(text)Или:
len([]rune(text))Второй способ создаёт slice runes, поэтому для простого подсчёта
utf8.RuneCountInString выражает намерение точнее.
Ни один из способов не гарантирует число визуальных пользовательских символов для всех комбинаций Unicode.
19. Преобразование между string, []byte и []rune
text := "Привет"
bytes := []byte(text)runes := []rune(text)
fmt.Println(bytes)fmt.Println(runes)Обратное преобразование:
textFromBytes := string(bytes)textFromRunes := string(runes)Для байтовой обработки файлов, сети и кодировок используют []byte. Для
изменения Unicode code points удобен []rune:
runes := []rune("Кот")runes[0] = 'Р'fmt.Println(string(runes)) // РотПреобразование обычно создаёт новые данные, поэтому его не следует выполнять без необходимости внутри горячего цикла.
20. Некорректный UTF-8
Строка может содержать произвольные байты, не только корректный UTF-8:
text := string([]byte{0xff, 0xfe, 'A'})fmt.Println(utf8.ValidString(text)) // falseПри переборе некорректные последовательности заменяются на utf8.RuneError —
символ �.
Проверка внешних данных:
if !utf8.Valid(data) { return fmt.Errorf("input is not valid UTF-8")}21. Пакет strings
Частые операции:
text := " Go, Python, Swift "
fmt.Println(strings.TrimSpace(text))fmt.Println(strings.ToUpper(text))fmt.Println(strings.ToLower(text))fmt.Println(strings.Contains(text, "Python"))fmt.Println(strings.HasPrefix(text, " Go"))fmt.Println(strings.HasSuffix(text, " "))Разбиение:
parts := strings.Split("go,python,swift", ",")Объединение:
joined := strings.Join(parts, " | ")Разбиение по последовательностям пробельных символов:
words := strings.Fields(" Go\tPython\nSwift ")Замена:
result := strings.ReplaceAll("go go go", "go", "Go")Сравнение без учёта регистра:
fmt.Println(strings.EqualFold("GO", "go"))EqualFold учитывает Unicode лучше, чем ручное сравнение двух результатов
ToLower.
22. Пакет unicode
Классификация runes:
for _, symbol := range "Go 2026!" { switch { case unicode.IsLetter(symbol): fmt.Println(symbol, "letter") case unicode.IsDigit(symbol): fmt.Println(symbol, "digit") case unicode.IsSpace(symbol): fmt.Println(symbol, "space") default: fmt.Println(symbol, "other") }}Также доступны unicode.ToUpper, unicode.ToLower и другие функции.
23. Конкатенация строк
Для нескольких частей оператор + понятен:
message := "Привет, " + name + "!"Для большого количества частей повторная конкатенация в цикле создаёт лишние строки:
result := ""for _, word := range words { result += word}Используйте strings.Builder:
var builder strings.Builder
for index, word := range words { if index > 0 { builder.WriteString(", ") } builder.WriteString(word)}
result := builder.String()После передачи непустого strings.Builder не следует копировать по значению.
24. Нормализация пользовательского ввода
Для командного интерфейса часто достаточно:
command := strings.TrimSpace(input)command = strings.ToLower(command)Теперь строки " Add ", "ADD" и "add" обрабатываются одинаково.
Для полноценной лингвистической нормализации Unicode могут потребоваться
дополнительные библиотеки. ToLower не решает все задачи поиска и сравнения во
всех языках.
25. Практический пример: частота слов
package main
import ( "fmt" "slices" "strings" "unicode")
func normalizeWord(word string) string { word = strings.TrimFunc(word, func(symbol rune) bool { return !unicode.IsLetter(symbol) && !unicode.IsDigit(symbol) }) return strings.ToLower(word)}
func wordFrequency(text string) map[string]int { frequency := make(map[string]int)
for _, rawWord := range strings.Fields(text) { word := normalizeWord(rawWord) if word == "" { continue } frequency[word]++ }
return frequency}
func main() { text := "Go прост. Go быстр, а Go-код форматируется автоматически." frequency := wordFrequency(text)
words := make([]string, 0, len(frequency)) for word := range frequency { words = append(words, word) } slices.Sort(words)
for _, word := range words { fmt.Printf("%-20s %d\n", word, frequency[word]) }}Этот простой алгоритм не является полноценным токенизатором: дефис внутри слова не удаляется. Ограничения решения нужно понимать и документировать.
26. Практический пример: уникальные теги
func uniqueTags(tags []string) []string { seen := make(map[string]struct{}, len(tags)) result := make([]string, 0, len(tags))
for _, tag := range tags { normalized := strings.ToLower(strings.TrimSpace(tag)) if normalized == "" { continue }
if _, exists := seen[normalized]; exists { continue }
seen[normalized] = struct{}{} result = append(result, normalized) }
return result}Map обеспечивает быструю проверку повторов, а slice сохраняет порядок первого появления тегов.
27. Типичные ошибки
Запись в nil map
Перед записью создайте map через литерал или make.
Проверка наличия только по zero value
Используйте value, ok, если нулевое значение допустимо.
Зависимость от порядка map
Для стабильного вывода сортируйте ключи.
Ожидание независимой копии
Присваивание map сохраняет общие данные. Используйте maps.Clone, если нужна
отдельная верхнеуровневая map.
Индексация строки как символов
text[index] возвращает байт. Для Unicode используйте range или []rune.
Ожидание, что len возвращает количество букв
Для строки len возвращает байты.
Квадратичная конкатенация
При построении большой строки применяйте strings.Builder или strings.Join.
Наивная обработка Unicode
Один rune не всегда равен одному визуальному символу, а перевод регистра не является полной нормализацией текста.
28. Вопросы для самопроверки
- Какие типы можно использовать как ключ map?
- Что возвращает чтение отсутствующего ключа?
- Зачем нужна форма
value, ok? - Какие операции безопасны для
nilmap? - Почему порядок обхода map нельзя считать заданным?
- Создаёт ли присваивание map копию данных?
- Как представить множество в Go?
- Что возвращает индексирование строки?
- Чем
byteотличается отrune? - Что возвращает
lenдля строки? - Как корректно перебрать Unicode code points?
- Когда использовать
[]byte, а когда[]rune? - Для чего нужен
strings.Builder? - Почему число runes не всегда равно числу видимых символов?
Итоги
Map связывает сравнимые ключи со значениями и возвращает zero value при
отсутствующем ключе; форму value, ok используют, когда наличие нужно отличить
от нулевого значения. Строка Go является неизменяемой последовательностью байтов,
обычно содержащей UTF-8. Корректная текстовая обработка требует понимать границы
байтов и runes и применять range, strings, unicode и unicode/utf8.