Строка — это байты
Сергей передал Мише валидацию имён файлов. Первый же тест упал на кириллице: счётчик насчитал в «привет» двенадцать символов вместо шести. Миша пошёл смотреть, что на самом деле лежит в строке.
в Go строка — неизменяемая последовательность байтов, поэтому len считает байты, а s[i] отдаёт байт, а не символ.
В большинстве языков, из которых приходят в Go, строка — это в том или ином виде массив символов: взял символ по индексу, узнал длину, всё сошлось. В Go эта привычка ломается сразу, и лучше сломать её здесь, пока она не потянула за собой неверную картину всего остального языка.
Строка в Go — это неизменяемая последовательность байтов. Не символов и не «букв», а именно байтов. Всё, что выглядит странным при первом знакомстве, вытекает из этой фразы.
В стандартной реализации строка внутри устроена как два слова: указатель на массив байтов и длина этого массива. Сам массив лежит где-то в куче или в статической памяти, а когда вы передаёте строку в функцию, копируются только эти два слова. Поэтому строки дёшево передавать и возвращать.
ASCII
Пока в строке только латиница, разницы между «байтами» и «символами» не видно — один символ занимает один байт.
Что здесь показано
- Стек — локальные переменные функции.
i = 3— значение;p = ptr → 10— указатель на значение;b = "hello"— срез (заголовок: указатель + длина + ёмкость). - Куча — данные, на которые ссылаются переменные:
rodata— байты строки-литерала (в бинарнике),данные N Б— бэкинг-массив слайса,строка— динамическая строка. - Горутины — лёгкие потоки Go: статус (running/runnable/waiting…), функция, в которой каждая сейчас находится, размер стека (растёт по мере работы) и пометка «из main» — горутина запущена из твоего кода.
- GC — сборщик мусора: фаза (off/mark) и число завершённых циклов.
Из «последовательности байтов» сразу следует, что len(s) возвращает количество байтов, а s[i] — это i-й байт, а не i-й символ. Тип у него byte, псевдоним uint8.
Кириллица
Разница появляется, как только строка перестаёт быть ASCII. Go хранит строки в кодировке UTF-8, где один символ может занимать от одного до четырёх байтов.
Что здесь показано
- Стек — локальные переменные функции.
i = 3— значение;p = ptr → 10— указатель на значение;b = "hello"— срез (заголовок: указатель + длина + ёмкость). - Куча — данные, на которые ссылаются переменные:
rodata— байты строки-литерала (в бинарнике),данные N Б— бэкинг-массив слайса,строка— динамическая строка. - Горутины — лёгкие потоки Go: статус (running/runnable/waiting…), функция, в которой каждая сейчас находится, размер стека (растёт по мере работы) и пометка «из main» — горутина запущена из твоего кода.
- GC — сборщик мусора: фаза (off/mark) и число завершённых циклов.
Каждая кириллическая буква здесь занимает два байта. s[0] возвращает первый байт первой буквы, и попытка напечатать его как символ даёт бессмыслицу. Это не ошибка Go — вы сами попросили байт. Индексация по строке всегда работает с байтами, и это гарантировано спецификацией, а не деталь реализации.
Вы узнаете:
- как по первому байту понять, сколько байтов занимает символ UTF-8
- как
len(s)соотносится с видимой длиной строки
Проверьте себя
Валидатор «Полки» считает длину имени. Что вернёт len("привет")?
Для строки s := "привет" чему равен s[0]?
Сколько байтов может занимать один символ в UTF-8?
правильных ответов
Строка — байты в кодировке UTF-8. len и индексация работают с байтами: для ASCII это незаметно, для кириллицы и прочего Unicode — критично. Дальше разберёмся, что значит неизменяемость и что из неё следует.
10 000 ₽ · 60 минут · онлайн