Битые байты, []rune и подсчёт символов
Валидация с лимитом двести пятьдесят шесть символов резала длинные имена. После обрезки в части имён внутри лежал мусор, и подсчёт символов в интерфейсе врал. Аня нашла пример у клиента и приложила его к тикету.
range гарантированно не паникует на мусоре, а []rune и RuneCountInString дают явное представление «по символам» — ценой копирования или линейного прохода.
Что делает range с кривыми данными? UTF-8 устроен так: первый байт символа своим префиксом кодирует длину, остальные байты символа начинаются с 10 и называются байтами продолжения. Если в строке попался битый кусок — недопустимый байт или оборванная последовательность — range отдаёт руну U+FFFD, символ замены �, и сдвигается на один байт вперёд. Это гарантировано спецификацией: перебор строки не паникует на мусоре, максимум — молча подменяет его символом замены.
[]rune распаковывает строку
[]rune(s) декодирует строку и раскладывает code point'ы по отдельным элементам слайса:
Что здесь показано
- Стек — локальные переменные функции.
i = 3— значение;p = ptr → 10— указатель на значение;b = "hello"— срез (заголовок: указатель + длина + ёмкость). - Куча — данные, на которые ссылаются переменные:
rodata— байты строки-литерала (в бинарнике),данные N Б— бэкинг-массив слайса,строка— динамическая строка. - Горутины — лёгкие потоки Go: статус (running/runnable/waiting…), функция, в которой каждая сейчас находится, размер стека (растёт по мере работы) и пометка «из main» — горутина запущена из твоего кода.
- GC — сборщик мусора: фаза (off/mark) и число завершённых циклов.
Здесь шесть букв, и []rune(s) даёт слайс из шести int32. Это копирование: как и []byte(s), преобразование аллоцирует новый слайс, потому что руны — это распакованное представление, а в исходной строке символы лежат упакованными. Обратное преобразование string(r) кодирует руны обратно в UTF-8.
Подсчёт символов
Если нужно просто посчитать символы, тащить целый слайс не обязательно. Есть utf8.RuneCountInString:
Что здесь показано
- Стек — локальные переменные функции.
i = 3— значение;p = ptr → 10— указатель на значение;b = "hello"— срез (заголовок: указатель + длина + ёмкость). - Куча — данные, на которые ссылаются переменные:
rodata— байты строки-литерала (в бинарнике),данные N Б— бэкинг-массив слайса,строка— динамическая строка. - Горутины — лёгкие потоки Go: статус (running/runnable/waiting…), функция, в которой каждая сейчас находится, размер стека (растёт по мере работы) и пометка «из main» — горутина запущена из твоего кода.
- GC — сборщик мусора: фаза (off/mark) и число завершённых циклов.
len — O(1): длина строки уже лежит в её заголовке. RuneCountInString — O(n): ей приходится пройти по строке и декодировать, другого способа узнать количество рун нет, поскольку они переменной ширины.
Разворот строки
Модель рун меняет код, который раньше был побайтовым. Возьмём разворот строки. Наивная побайтовая версия для «привет» выдаёт мусор, потому что разворачивает байты, разрезая двухбайтовые символы пополам. Правильно разворачивать руны:
func reverse(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}
Тот же самый алгоритм, но единица обмена — руна, поэтому многобайтовые символы переезжают целиком. За это платишь: []rune(s) — это аллокация и полный проход по строке, плюс линейный проход по рунам и кодирование обратно.
Вы узнаете:
- почему недопустимая руна при
string(r)превращается в U+FFFD - почему составные символы (emoji) рунами не считаются
- как обойти строку без промежуточного слайса для больших объёмов
Проверьте себя
Что делает range, когда в строке попадается битый байт?
Чем []rune(s) отличается от исходной строки?
Что дешевле: len(s) или utf8.RuneCountInString(s)?
правильных ответов
range безопасен на мусоре (подставляет U+FFFD); явная работа «по символам» — через []rune или RuneCountInString — стоит копии или прохода O(n).
10 000 ₽ · 60 минут · онлайн