Что такое руна (rune) в Go и чем она отличается от byte?
Краткий ответ: rune — это Unicode-символ в Go
Тип rune в Go — это алиас (псевдоним) для типа int32. Он используется для представления одного Unicode-символа (code point). По сути, rune — это целое число от 0 до 0x10FFFF, которое кодирует любой символ из стандарта Unicode, включая буквы, цифры, иероглифы, эмодзи и т.д.
Теперь разберём вашу проблему по шагам.
Почему простой цикл сломался?
Строки в Go — это неизменяемые последовательности байтов (тип string — это []byte, только read-only). Когда вы пишете s[i], вы получаете один байт, а не символ. Проблема в том, что кириллица (как и многие другие символы) кодируется в UTF-8 несколькими байтами:
- Символ «П» (кириллическая заглавная) в UTF-8 занимает 2 байта:
0xD0 0x9F - Символ «р» — 2 байта:
0xD1 0x80 - И так далее
Когда вы делаете s[0], вы получаете только первый байт (0xD0), который сам по себе не является корректным символом. Поэтому вывод идёт кракозябрами.
Как работает range?
Конструкция for i, r := range s специально разработана для итерации по строкам с учётом UTF-8. Она автоматически декодирует каждый Unicode-символ из последовательности байтов и возвращает:
i— позицию начала символа в байтахr— сам символ какrune
Пример:
s := "Привет, мир!"
for i, r := range s {
fmt.Printf("Позиция %d: %c (rune = %U, код = %d)\n", i, r, r, r)
}
// Вывод:
// Позиция 0: П (rune = U+041F, код = 1055)
// Позиция 2: р (rune = U+0440, код = 1088)
// ... обратите внимание, что индексы идут с шагом 2, потому что каждый символ занимает 2 байта
Разница между byte и rune
Простая таблица:
| Характеристика | byte | rune |
|---|---|---|
| Размер | 8 бит (1 байт) | 32 бита (4 байта) |
| Диапазон | 0–255 | 0–0x10FFFF (все Unicode) |
| Что хранит | Один байт (часто ASCII-символ) | Один Unicode-символ |
| Псевдоним | uint8 | int32 |
| Типичное применение | Работа с бинарными данными, ASCII | Работа с текстом, интернационализация |
Когда использовать byte, а когда rune?
Используйте byte, когда:
- Работаете с бинарными протоколами, файлами, сетью
- Гарантированно имеете дело только с ASCII (английские буквы, цифры, базовые знаки)
- Нужна максимальная производительность при обработке больших объёмов данных
Используйте rune, когда:
- Работаете с текстом на любом языке (русский, китайский, арабский)
- Нужны эмодзи или спецсимволы (например, ©, ®, ♥)
- Требуется корректная обработка строк: подсчёт символов, обрезка, поиск
Практический пример: подсчёт символов
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
s := "Привет, 🌍!"
// Неправильный способ — считает байты
fmt.Println("len(s):", len(s)) // 16 байт
// Правильный способ 1 — через utf8.RuneCountInString
fmt.Println("utf8.RuneCountInString:", utf8.RuneCountInString(s)) // 9 символов
// Правильный способ 2 — через range
count := 0
for range s {
count++
}
fmt.Println("Через range:", count) // 9 символов
}
Важное замечание про производительность
Не злоупотребляйте преобразованием строки в []rune ([]rune(s)) — это выделяет новую память и копирует все данные. Если нужно просто пройтись по символам, используйте range. Преобразование оправдано только если вам нужен доступ по индексу символа (а не байта).
Надеюсь, теперь стало понятнее! Rune — это просто способ сказать компилятору: «эй, я тут работаю с Unicode-символами, а не с байтами».
<<Дополнение: rune как int32 и ручное декодирование
Первый ответ отлично объяснил основы. Я хочу добавить про то, что rune — это просто число, и как можно работать с ним вручную.
Когда вы получаете rune из строки, это на самом деле код символа из таблицы Unicode:
package main
import "fmt"
func main() {
r := 'Я' // rune-литерал (в одинарных кавычках)
fmt.Printf("Символ: %c\n", r) // Я
fmt.Printf("Код: %d\n", r) // 1071
fmt.Printf("HEX: %U\n", r) // U+042F
}
Вы можете сравнивать руны, сортировать их, использовать в switch — всё как с обычными числами:
func isRussianLetter(r rune) bool {
return (r >= 'А' && r <= 'я') || r == 'Ё' || r == 'ё'
}
Если по какой-то причине вы не хотите использовать range, можно декодировать строку вручную через пакет unicode/utf8:
s := "Привет"
for i := 0; i < len(s); {
r, size := utf8.DecodeRuneInString(s[i:])
fmt.Printf("Символ: %c, занимает %d байт\n", r, size)
i += size
}
Этот способ даёт тот же результат, что и range, но даёт больше контроля (например, можно пропускать некорректные последовательности). Однако на практике range — это идиоматичный и предпочтительный способ.
Запомните главное: строка в Go — это байты, а range превращает байты в символы (руны).
Ответов пока нет.
Войдите чтобы оставить ответ
Блоги