Материал проходит вычитку — возможны неточности.
Урок 21 из 118

Битые байты, []rune и подсчёт символов

Валидация с лимитом двести пятьдесят шесть символов резала длинные имена. После обрезки в части имён внутри лежал мусор, и подсчёт символов в интерфейсе врал. Аня нашла пример у клиента и приложила его к тикету.

Главная мысль

range гарантированно не паникует на мусоре, а []rune и RuneCountInString дают явное представление «по символам» — ценой копирования или линейного прохода.

Что делает range с кривыми данными? UTF-8 устроен так: первый байт символа своим префиксом кодирует длину, остальные байты символа начинаются с 10 и называются байтами продолжения. Если в строке попался битый кусок — недопустимый байт или оборванная последовательность — range отдаёт руну U+FFFD, символ замены �, и сдвигается на один байт вперёд. Это гарантировано спецификацией: перебор строки не паникует на мусоре, максимум — молча подменяет его символом замены.

[]rune распаковывает строку

[]rune(s) декодирует строку и раскладывает code point'ы по отдельным элементам слайса:

Go

Здесь шесть букв, и []rune(s) даёт слайс из шести int32. Это копирование: как и []byte(s), преобразование аллоцирует новый слайс, потому что руны — это распакованное представление, а в исходной строке символы лежат упакованными. Обратное преобразование string(r) кодирует руны обратно в UTF-8.

Подсчёт символов

Если нужно просто посчитать символы, тащить целый слайс не обязательно. Есть utf8.RuneCountInString:

Go

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) рунами не считаются
  • как обойти строку без промежуточного слайса для больших объёмов
Задание · вопросы

Проверьте себя

Вопрос 1 из 3

Что делает range, когда в строке попадается битый байт?

Вопрос 2 из 3

Чем []rune(s) отличается от исходной строки?

Вопрос 3 из 3

Что дешевле: len(s) или utf8.RuneCountInString(s)?

Итоги

range безопасен на мусоре (подставляет U+FFFD); явная работа «по символам» — через []rune или RuneCountInString — стоит копии или прохода O(n).

Наш сайт собирает куки. Чтобы тебе было удобнее им пользоваться. Нажимая на кнопку, ты даёшь разрешение собирать куки