Avatar ·

Что такое руна (rune) в Go и чем она отличается от byte?

Всем привет! Я только начал изучать Go и наткнулся на странный тип данных — rune. Читал документацию, но до конца не понял, зачем он нужен. Я пытаюсь работать со строками и iterировать по ним. Написал такой код: ```go package main import "fmt" func main() { s := "Привет, мир!" for i := 0; i < len(s); i++ { fmt.Printf("%c ", s[i]) } } ``` Но вместо ожидаемых символов я получил какую-то кашу: `П Ð Ð¸ в е Ñ , м и Ñ !`. Я ожидал увидеть каждый символ строки по порядку, но вывод явно не соответствует тому, что я хотел. Почему так происходит? Потом я заменил цикл на `for i, r := range s`, и всё заработало правильно. В документации написано, что `r` имеет тип `rune`. Объясните, пожалуйста: 1. Что такое `rune` простыми словами? 2. В чём разница между `byte` и `rune`? 3. Когда нужно использовать `rune`, а когда `byte`? 4. Почему простой цикл с индексацией не работает для кириллицы, а `range` работает? Заранее спасибо! <<>>

Краткий ответ: 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

Простая таблица:

Характеристикаbyterune
Размер8 бит (1 байт)32 бита (4 байта)
Диапазон0–2550–0x10FFFF (все Unicode)
Что хранитОдин байт (часто ASCII-символ)Один Unicode-символ
Псевдонимuint8int32
Типичное применениеРабота с бинарными данными, 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 превращает байты в символы (руны).