Перейти к содержимому
Теория · Строки · урок 3 из 8

Руны и range по строке

В прошлом уроке строка была рядом фишек, где буква кириллицы занимает две фишки. Функция len считает фишки-байты, поэтому для слова на кириллице она возвращает вдвое больше, чем в нём букв. Чтобы считать и перебирать именно символы, в Go есть отдельная функция и отдельный тип:

GO
package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    s := "мир"
    fmt.Println(len(s), utf8.RuneCountInString(s)) // 6 3
}

Давайте разберём вывод:

  1. len(s) возвращает 6: в слове "мир" три буквы по два байта.
  2. 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 — номер байта, с которого она начинается. Поэтому для двухбайтовых букв индексы идут через один:

GO
for i, r := range "мир" {
    fmt.Print(i, ":", string(r), " ") // 0:м 2:и 4:р
}

Давайте разберём вывод по буквам:

  1. Буква м начинается с байта 0 и занимает байты 0 и 1.
  2. Поэтому и начинается с байта 2.
  3. А р — с байта 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) проходит всю строку, поэтому его делают один раз до цикла, а не на каждой итерации.
GO
rs := []rune("мир")
rs[0] = 'п'
fmt.Println(len(rs), string(rs)) // 3 пир

Давайте разберём пример по строкам:

  1. rs := []rune("мир") даёт срез из трёх рун.
  2. rs[0] = 'п' меняет первую руну: срез, в отличие от строки, менять можно.
  3. string(rs) собирает новую строку, и программа печатает 3 пир.

Руна и видимая буква

У картинки «руна — целая буква» есть предел. Не каждый знак, который видит человек, — одна руна. Букву é можно записать и одной руной, и двумя: буквой e и отдельным значком ударения. Во втором случае utf8.RuneCountInString насчитает две руны, хотя на экране видна одна буква.

Частая ошибка

Длину имени проверяют функцией len, хотя ограничение задано в буквах:

GO
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 и подпишись на канал — откроются все уроки.

Войти другим способом