Перейти к основному содержимому

Строки, руны и 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) — копия всех рун
[]runestringstring(runes)
string[]byte[]byte(s) — копия байтов
Без копии байтов (осторожно)unsafe или работа с []byte изначально

Пакет strings

Часто используемые функции:

  • Contains, HasPrefix, HasSuffix, TrimSpace, TrimPrefix
  • Split, Join, Replace, ReplaceAll
  • Compare — лексикографическое сравнение байтов (не локаль!)
  • 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.


Типичные ошибки

  1. Срез по байтамs[:3] для обрезки "трёх символов" кириллицы.
  2. Индексация в цикле for i := 0; i < len(s); i++ — обход байтов, не символов (допустимо для ASCII-протоколов).
  3. Путать len и количество символов в UI и лимитах полей.
  4. Регулярные выражения — в Go regexp работает с индексами байт; для Unicode-символов иногда нужны классы \p{L}.

Практические сценарии

СценарийПодход
Лог, протокол ASCII[]byte, len, индексы байт
Пользовательский ввод, UIrange, utf8, нормализация
JSON APIstring как 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 полезен для дедупликации данных и мягкой валидации формы.