Руны и range по строке
В прошлом уроке строка была рядом фишек, где буква кириллицы занимает две фишки. Функция len считает фишки-байты, поэтому для слова на кириллице она возвращает вдвое больше, чем в нём букв. Чтобы считать и перебирать именно символы, в Go есть отдельная функция и отдельный тип:
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
s := "мир"
fmt.Println(len(s), utf8.RuneCountInString(s)) // 6 3
}Давайте разберём вывод:
len(s)возвращает6: в слове"мир"три буквы по два байта.utf8.RuneCountInString(s)считает целые буквы и возвращает3.
Руна — это символ
Руна — номер символа в таблице Unicode, значение типа rune. В картинке с фишками руна — одна целая буква, сколько бы фишек она ни заняла. Unicode — общий для всех языков список символов, где у каждого свой номер. UTF-8 записывает этот номер одним–четырьмя байтами.
Как руна выглядит в коде:
- одинарные кавычки задают как раз руну:
'м'— это число1084; - тип
rune— другое имя типаint32, поэтому руны сравнивают оператором==, как числа; - литерал в одинарных кавычках подстраивается под тип, с которым его сравнивают.
Поэтому s[0] == 'G' компилируется, а сравнение байта s[i] с переменной типа rune — нет.
Подсчёт символов
Функция utf8.RuneCountInString(s) из пакета unicode/utf8 возвращает число рун — символов строки. Буква é тоже занимает в UTF-8 два байта, поэтому в строке "café" пять байтов и четыре руны.
Этот вопрос часто задают на собеседовании: «Чем byte отличается от rune?» Ответ состоит из двух частей:
- байт — одна фишка, число от
0до255, кусочек записи текста; - руна — номер целого символа в Unicode, и в UTF-8 он занимает от одного до четырёх байтов.
Перебор строки
Строку можно пройти циклом двумя способами: по рунам и по байтам. Давайте разберём каждый.
range по рунам
Цикл for i, r := range s перебирает строку по рунам. В r попадает очередная руна, а в i — номер байта, с которого она начинается. Поэтому для двухбайтовых букв индексы идут через один:
for i, r := range "мир" {
fmt.Print(i, ":", string(r), " ") // 0:м 2:и 4:р
}Давайте разберём вывод по буквам:
- Буква
мначинается с байта0и занимает байты0и1. - Поэтому
иначинается с байта2. - А
р— с байта4.
Преобразование string(r) превращает руну в строку из одного символа. Без него fmt.Print напечатал бы номер руны, а не букву. Если индекс не нужен, вместо его имени пишут _: for _, r := range s.
Цикл по байтам
Обычный цикл for i := 0; i < len(s); i++ идёт по байтам. Он перебирает фишки по одной, и половинки букв не склеивает.
Для "мир" такой цикл сделает шесть шагов, и s[i] на каждом шаге даст один байт, а не букву. Подходит он, когда в строке только латиница, цифры и пробелы: там каждый символ занимает один байт.
Срез рун
Чтобы обращаться к символам по номеру, строку преобразуют в срез рун []rune(s). Это как снять буквы с нитки и разложить по ячейкам, по одной целой букве в каждой. Свойства такого среза:
- каждый его элемент — целый символ, а
lenвозвращает число символов; - срез рун — это копия, и его элементы можно менять;
- обратное преобразование
string(rs)собирает из среза новую строку; - преобразование
[]rune(s)проходит всю строку, поэтому его делают один раз до цикла, а не на каждой итерации.
rs := []rune("мир")
rs[0] = 'п'
fmt.Println(len(rs), string(rs)) // 3 пирДавайте разберём пример по строкам:
rs := []rune("мир")даёт срез из трёх рун.rs[0] = 'п'меняет первую руну: срез, в отличие от строки, менять можно.string(rs)собирает новую строку, и программа печатает3 пир.
Руна и видимая буква
У картинки «руна — целая буква» есть предел. Не каждый знак, который видит человек, — одна руна. Букву é можно записать и одной руной, и двумя: буквой e и отдельным значком ударения. Во втором случае utf8.RuneCountInString насчитает две руны, хотя на экране видна одна буква.
Частая ошибка
Длину имени проверяют функцией len, хотя ограничение задано в буквах:
name := "Анна"
if len(name) > 4 { // ✗
fmt.Println("too long")
}Программа печатает too long, хотя в имени четыре буквы: len возвращает 8, по два байта на букву. Символы считает utf8.RuneCountInString(name).
Главное
- Руна — номер символа в Unicode, тип
rune(другое имяint32); байт — кусочек его записи в UTF-8. utf8.RuneCountInString(s)считает символы, аlen(s)— байты.for i, r := range sперебирает строку по рунам, аi— номер байта, с которого руна начинается.[]rune(s)даёт срез символов, который можно менять, аstring(rs)собирает из него новую строку.
Подпишись, чтобы пройти курс
Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.