В текстовом обработчике нужно пройти строку с символами вне ASCII. Почему обход через range и обращение к строке по индексу дают разные единицы данных?
Индексация строки в Go возвращает отдельный байт, а range декодирует строку как последовательность UTF-8-кодовых точек и возвращает значение типа rune. Поэтому один символ может занимать несколько байт: индекс посетит их по отдельности, а range обычно выдаст один rune.
В Go строка намеренно представлена как неизменяемая последовательность байтов, а не как массив символов фиксированного размера. Это удобно для эффективной работы с бинарными данными, протоколами и ASCII-текстом, но для Unicode требуется отдельный механизм декодирования.
Такое разделение позволяет явно выбирать операцию: индексирование работает на уровне байтов, а range — на уровне UTF-8-кодовых точек. При этом rune — это псевдоним для int32, предназначенный для хранения Unicode-кодовой точки.
Если считать каждый байт отдельным символом, можно неверно вычислить длину текста, обрезать UTF-8-последовательность посередине или получить повреждённые данные. Например, визуально один символ может занимать от одного до четырёх байтов.
Обратная ошибка тоже возможна: если нужен размер данных в байтах для сетевого протокола или файла, подсчёт rune не даст нужного результата. Нельзя без уточнения задачи заменять байтовую обработку обработкой Unicode-кодовых точек.
При обращении по индексу результат имеет тип byte и содержит байт с указанной позицией. Индекс не обязан совпадать с номером Unicode-символа, потому что UTF-8 использует переменное количество байтов.
range идёт по байтовым позициям, распознаёт UTF-8-последовательность и возвращает пару: индекс начала последовательности в байтах и декодированное значение rune. Следующий индекс поэтому может увеличиться сразу на несколько байтов.
В этом примере len(s) возвращает количество байтов, а не количество символов. Индекс s[2] обращается к первому байту многобайтового символа, тогда как range возвращает этот символ целиком как rune.
Некорректная UTF-8-последовательность при обходе через range заменяется на unicode.ReplacementChar, обычно U+FFFD; для такой ошибочной последовательности шаг составляет один байт. Если нужны именно байты, следует использовать индексацию или преобразование к []byte; если нужны Unicode-кодовые точки — range или []rune.
Важно, что Unicode-кодовая точка не всегда равна пользовательскому символу: один отображаемый символ может состоять из нескольких кодовых точек, например из базовой буквы и комбинируемого знака. Для подсчёта визуальных графем требуется более сложная Unicode-обработка.
Сервис ограничивает имя пользователя длиной в 20 единиц. Вариант с len прост и быстро ограничивает размер в байтах, но может отклонить короткое по числу символов имя. Вариант с подсчётом элементов range считает кодовые точки, однако всё ещё не учитывает составные графемы.
Если ограничение связано с размером поля в протоколе или базе данных, выбирают подсчёт байтов. Если это пользовательское ограничение по числу Unicode-кодовых точек, используют range; для строгого ограничения видимых символов применяют библиотеку, умеющую работать с графемными кластерами.
Практически разумное решение — явно назвать единицу ограничения в требованиях и тестах. Это предотвращает ситуацию, когда разработчик применяет len там, где бизнес-логика ожидает количество символов.
Какой индекс возвращает range для строки?
Это индекс начала текущей UTF-8-последовательности в исходной строке, то есть позиция в байтах, а не порядковый номер rune. Поэтому индексы в цикле могут иметь пропуски.
Что произойдёт при обходе некорректной UTF-8-строки?
Строка в Go может содержать произвольные байты и не обязана быть корректным UTF-8-текстом. range обнаруживает ошибочную последовательность, возвращает unicode.ReplacementChar и продвигается по соответствующим байтам; это не исправляет исходную строку.
Равны ли количество rune и количество видимых символов?
Нет. Кодовая точка — лишь элемент Unicode-представления, а видимый символ может состоять из нескольких кодовых точек или требовать особого учёта объединённых последовательностей. Поэтому utf8.RuneCountInString и подсчёт графем отвечают на разные вопросы.