Строки, руны и Unicode в Go
Работа со строками в Go кажется простой до момента, когда появляются кириллица, эмодзи и пользовательские имена из разных языков. Этот раздел помогает избежать типичных ошибок на границе "байты vs символы".
См. также: Типы данных и переменные · Справочник.
Модель строки
Тип string — неизменяемая последовательность байтов в кодировке UTF-8. Это не массив "символов":
len(s)— число байт, не рун;- индекс
s[i]— один байт (byte, алиасuint8), не символ; - срез
s[0:2]может разрезать многобайтовый символ пополам и дать невалидный UTF-8.
Тип rune — алиас int32 для кодовой точки Unicode (U+XXXX). Литерал: 'Ж', '🙂'.
Итерация по символам
s := "café"
for i, r := range s {
fmt.Printf("байт %d: U+%04X %c\n", i, r, r)
}
Разбор:
s := "café"создаёт UTF-8 строку, где часть символов может занимать больше одного байта.- Цикл
for i, r := range sитерирует строку по рунам, а не по отдельным байтам. i— индекс байта, с которого начинается текущая руна в исходной строке.r— значение кодовой точки Unicode (rune), пригодное для символьной обработки.fmt.Printf(...)выводит сразу три представления: смещение, кодовую точкуU+...и символ.- Фрагмент наглядно показывает разницу между длиной в байтах и количеством символов.
range по строке декодирует UTF-8 и отдаёт (индекс байта начала руны, rune). Количество итераций — число рун, не len(s).
Подсчёт рун без аллокации:
n := utf8.RuneCountInString(s)
Разбор:
utf8.RuneCountInString(s)считает количество рун в строке, а не число байтов.- Это безопасный способ валидировать лимиты "по символам" для пользовательского ввода.
- В отличие от
len(s), результат корректен для кириллицы, акцентов и эмодзи. - Функция подходит для коротких проверок без ручного цикла
range.
Пакет unicode/utf8: utf8.DecodeRuneInString, utf8.ValidString.
Сборка и изменение текста
Строки неизменяемы — конкатенация + в цикле создаёт много копий. Для накопления:
var b strings.Builder
b.Grow(estimatedSize) // опционально
b.WriteString("prefix")
b.WriteRune('—')
result := b.String()
Разбор:
strings.Builderпредназначен для эффективной по памяти сборки итоговой строки из частей.Grow(estimatedSize)заранее резервирует буфер и уменьшает количество перераспределений памяти.WriteString("prefix")добавляет готовый строковый фрагмент в текущий буфер.WriteRune('—')корректно добавляет Unicode-символ, который может занимать несколько байтов.String()возвращает собранный результат как неизменяемую строку.- Этот подход особенно полезен в циклах и генераторах текста, где много конкатенаций.
strings.Builder не копирует буфер при каждом добавлении (в отличие от []byte + append без предварительного Grow в горячих циклах).
Преобразования:
| Нужно | Код |
|---|---|
string → []rune | []rune(s) — копия всех рун |
[]rune → string | string(runes) |
string → []byte | []byte(s) — копия байтов |
| Без копии байтов (осторожно) | unsafe или работа с []byte изначально |
Пакет strings
Часто используемые функции:
Contains,HasPrefix,HasSuffix,TrimSpace,TrimPrefixSplit,Join,Replace,ReplaceAllCompare— лексикографическое сравнение байтов (не локаль!)EqualFold— регистронезависимое сравнение для ASCII и простых случаев Unicode
Поиск подстроки: strings.Index, strings.Count. Для множественных разделителей — strings.FieldsFunc.
Подстрока по рунам
Когда нужно "первые N символов", сначала переведите строку в []rune:
rs := []rune(s)
if len(rs) > 10 {
s = string(rs[:10])
}
Разбор:
[]rune(s)переводит строку в срез рун, чтобы индексация шла по символам Unicode.len(rs)теперь означает количество символов, а не число байтов в UTF-8.- Условие
> 10ограничивает длину строки для сценариев UI и валидации поля. rs[:10]берёт первые десять рун без разрезания символа посередине.string(...)обратно собирает корректную UTF-8 строку из выбранного диапазона.- Цена подхода — дополнительная аллокация памяти, которая оправдана корректностью.
Подход чуть дороже по памяти, но даёт корректный результат для Unicode-текста.
Сравнение и нормализация
Два визуально одинаковых текста могут состоять из разных последовательностей кодовых точек (например, буква "é" как один символ U+00E9 или как "e" + комбинирующий акут). Для надёжного сравнения имён, поиска, ключей:
import "golang.org/x/text/unicode/norm"
a := norm.NFC.String(inputA)
b := norm.NFC.String(inputB)
if a == b { /* ... */ }
Разбор:
- Импорт
normподключает механизмы Unicode-нормализации из пакетаx/text. norm.NFC.String(inputA)приводит строку к канонической форме NFC.- Аналогичная нормализация второй строки устраняет различия в техническом представлении одинаковых символов.
- Сравнение
a == bпосле нормализации даёт стабильный результат для многоязычных данных. - Такой приём важен для логина, поиска и дедупликации значений, введённых пользователем.
Пакет golang.org/x/text — вне стандартной библиотеки, но де-факто стандарт для i18n. Для чистого ASCII достаточно == или EqualFold.
Типичные ошибки
- Срез по байтам —
s[:3]для обрезки "трёх символов" кириллицы. - Индексация в цикле
for i := 0; i < len(s); i++— обход байтов, не символов (допустимо для ASCII-протоколов). - Путать
lenи количество символов в UI и лимитах полей. - Регулярные выражения — в Go
regexpработает с индексами байт; для Unicode-символов иногда нужны классы\p{L}.
Практические сценарии
| Сценарий | Подход |
|---|---|
| Лог, протокол ASCII | []byte, len, индексы байт |
| Пользовательский ввод, UI | range, utf8, нормализация |
| JSON API | string как UTF-8; валидировать на границе |
| Хеш или HMAC | []byte исходного UTF-8, не "нормализованная" строка без правила |
Теги json:"name" и сериализация — в работе с БД и структурами и интерфейсах.
Продолжение темы обработки текстовых данных в API: веб на стандартной библиотеке и фреймворки.
Дополнительные сниппеты с разбором
Пример — безопасное усечение строки для UI
func truncateRunes(s string, max int) string {
if max <= 0 {
return ""
}
rs := []rune(s)
if len(rs) <= max {
return s
}
return string(rs[:max]) + "…"
}
Разбор:
- Функция принимает исходную строку и лимит символов
max. - Проверка
max <= 0закрывает пограничный случай и возвращает пустую строку. []rune(s)переводит строку в последовательность Unicode-символов для корректной обрезки.- Условие
len(rs) <= maxсохраняет исходную строку, если усечение не требуется. string(rs[:max]) + "…"обрезает по рунам и добавляет многоточие как визуальный индикатор.- Подход предотвращает поломку UTF-8, которая бывает при срезах строки по байтам.
Пример — проверка валидности UTF-8
func isValidText(s string) bool {
return utf8.ValidString(s)
}
Разбор:
utf8.ValidStringпроверяет, является ли строка корректной UTF-8 последовательностью.- Функция возвращает
trueдля валидного текста иfalseдля повреждённых данных. - Такая проверка полезна на границе API при приёме небезопасного внешнего ввода.
- Мини-хелпер повышает читаемость кода в handlers и валидаторах.
- Результат проверки удобно использовать для раннего
400 Bad Request.
Пример — нормализация + регистронезависимое сравнение
func equalsUserInput(a, b string) bool {
na := norm.NFC.String(strings.TrimSpace(a))
nb := norm.NFC.String(strings.TrimSpace(b))
return strings.EqualFold(na, nb)
}
Разбор:
TrimSpaceудаляет случайные пробелы в начале и конце пользовательского ввода.norm.NFC.String(...)приводит обе строки к одной канонической форме Unicode.strings.EqualFoldвыполняет регистронезависимое сравнение после нормализации.- Комбинация трёх шагов даёт стабильное сравнение имён и ключей в многоязычном тексте.
- Такой helper полезен для дедупликации данных и мягкой валидации формы.