Как устроены строки в Go под капотом и что происходит при итерировании через range
Что проверяют
Понимаете ли вы, что строка в Go это не массив символов, а неизменяемая последовательность байтов в UTF-8, и что range идёт по рунам, а индекс отдаёт байтовый. Вопрос быстро отделяет тех, кто писал s[i] и удивлялся кракозябрам в логах. Следом обычно спрашивают, что вернёт len("Привет") и чем []byte(s) отличается от []rune(s).
Как отвечать
- представление: заголовок строки это указатель на байты и длина (
runtime.stringStruct), 16 байт на 64-битной платформе. Копирование строки копирует заголовок, а не данные; - неизменяемость: байты лежат в памяти только для чтения,
s[0] = 'A'не компилируется. Каждая конкатенация выделяет новый буфер, поэтому в цикле берутstrings.Builder; - кодировка: UTF-8, от 1 до 4 байт на руну.
len(s)возвращает байты, число рун считаетutf8.RuneCountInString; - индексация:
s[i]даётbyte, то естьuint8, а не символ; - range: на каждой итерации отдаёт байтовое смещение начала руны и саму руну типа
rune(int32), поэтому на кириллице индекс растёт через два; - битые байты: невалидная последовательность даёт
utf8.RuneError(U+FFFD) и сдвиг ровно на один байт, без паники.
Полезно добавить про графемы: акцентированная буква бывает одной руной, а бывает буквой плюс комбинирующий знак, эмодзи с модификатором цвета кожи это тоже несколько рун. Разворот строки по рунам такие последовательности ломает, для человеко-видимых символов нужна сегментация из golang.org/x/text.
Про производительность: range по строке ничего не выделяет, а []rune(s) и []byte(s) копируют, кроме случаев, которые компилятор умеет оптимизировать (for i, c := range []byte(s), ключ вида m[string(b)]). Обратная конверсия string(b) тоже копирует.
Где спотыкаются
Считают символы через len(s). На латинице сходится, на кириллице получают вдвое больше и потом режут строку по неверной границе.
Пишут for i := 0; i < len(s); i++ и обрабатывают полурунные байты. Отсюда обрезанные слова в превью и битые поля в базе.
Говорят, что range возвращает номер символа. Он возвращает смещение в байтах, и использование этого значения как счётчика итераций даёт неверный результат.