Avatar ·

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

Я только начал изучать Go и наткнулся на странную штуку — тип `rune`. Вроде бы это целое число, но зачем тогда отдельный тип? Я пытаюсь работать со строками и заметил, что когда я беру символ по индексу, то получаю странные числа, а не буквы. Вот пример, который меня смутил: ```go package main import "fmt" func main() { s := "Привет, мир!" fmt.Println(s[0]) // Выводит 208, а не 'П' fmt.Println(string(s[0])) // Выводит 'Ð', а не 'П' } ``` Почему так происходит? Я ожидал, что `s[0]` даст мне первый символ строки. Но вместо этого я получаю какой-то мусор. Я прочитал, что нужно использовать `rune`, но не понимаю, как именно. Пробовал так: ```go r := []rune(s) fmt.Println(string(r[0])) // Теперь выводит 'П' ``` Это работает, но я не понимаю, почему. Что такое `rune` на самом деле? Это просто `int32`? Или это что-то другое? Зачем вообще нужен этот тип, если можно обойтись байтами? Объясните, пожалуйста, как работают строки, байты и руны в Go. Когда нужно использовать `rune`, а когда можно обойтись `byte`? И почему мой первый пример не сработал? <<>>

Что такое rune в Go: полное объяснение

Отличный вопрос! Вы столкнулись с одной из ключевых особенностей Go — работой с Unicode. Давайте разберём всё по порядку.

1. Строки в Go — это байтовые срезы

В Go строка — это не массив символов, а срез байтов (по сути, []byte только read-only). Когда вы пишете s[0], вы получаете первый байт этой последовательности, а не первый символ.

Проблема в том, что русские буквы (и многие другие символы) занимают в UTF-8 не один байт, а несколько. Например, буква 'П' в UTF-8 кодируется двумя байтами: 208 159. Поэтому s[0] возвращает 208 — первый байт, а не символ.

2. Что такое rune?

rune — это встроенный тип в Go, который является псевдонимом для int32. Он представляет собой кодовую точку Unicode (code point). Проще говоря, одна руна = один символ (в большинстве случаев).

Когда вы конвертируете строку в []rune, Go декодирует UTF-8 последовательность и создаёт срез, где каждый элемент — это полноценный символ:

s := "Привет"
runes := []rune(s)
// runes[0] = 1055  (код буквы 'П')
// runes[1] = 1088  (код буквы 'р')
// и т.д.

3. Как правильно работать со строками?

Вот несколько подходов в зависимости от задачи:

  • Если вам нужны отдельные символы — используйте []rune:
s := "Привет, мир!"
runes := []rune(s)
for i, r := range runes {
    fmt.Printf("Символ %d: %c (код: %d)\n", i, r, r)
}
// Вывод:
// Символ 0: П (код: 1055)
// Символ 1: р (код: 1088)
// ...
  • Если нужно просто перебрать символы — используйте for range по строке:
s := "Привет"
for i, r := range s {
    fmt.Printf("Позиция %d: %c\n", i, r)
}
// Вывод:
// Позиция 0: П
// Позиция 2: р  (обратите внимание: позиции байтов, а не символов!)
// Позиция 4: и
  • Если работаете с ASCII символами (латиница, цифры) — можно использовать byte:
s := "Hello"
fmt.Println(s[0]) // 72 (код 'H') — работает, т.к. латиница = 1 байт

4. Когда использовать rune, а когда byte?

Вот простое правило:

  • byte — для работы с сырыми данными, бинарными протоколами, ASCII текстом
  • rune — для работы с текстом на естественных языках, эмодзи, любыми Unicode символами

Если вы пишете приложение, которое должно корректно обрабатывать русский, китайский, арабский или любой другой язык — используйте rune.

5. Почему ваш первый пример не сработал?

Потому что s[0] возвращает первый байт строки. Для многобайтовых символов это не целый символ. Преобразование string(s[0]) пытается интерпретировать один байт как строку UTF-8, что даёт некорректный результат.

Правильный способ получить первый символ:

s := "Привет"
firstRune := []rune(s)[0]
fmt.Println(string(firstRune)) // 'П'

Итог

rune — это просто int32, который хранит Unicode код символа. Go использует его, чтобы работать с текстом правильно, учитывая многобайтовую кодировку UTF-8. Запомните: строка — это байты, руна — это символ.

<<>>

Дополнение: производительность и практические советы

Хотя преобразование в []rune — это правильный способ работать с символами, помните о производительности. Конвертация строки в []rune требует O(n) времени и памяти, где n — длина строки в байтах. Для коротких строк это незаметно, но для больших текстов (мегабайты) может быть накладно.

Альтернативные подходы:

  • Итерация без преобразования — используйте for range по строке. Go сам декодирует UTF-8 на лету:
s := "Привет"
for _, r := range s {
    fmt.Printf("%c ", r) // П р и в е т
}
  • Пакет unicode/utf8 — для продвинутой работы:
import "unicode/utf8"

s := "Привет"
r, size := utf8.DecodeRuneInString(s)
fmt.Printf("Символ: %c, размер: %d байт\n", r, size) // П, 2 байта
  • Срез строки по байтам — если вы точно знаете границы символов:
s := "Привет"
fmt.Println(s[:2]) // "П" (первые 2 байта)

Но будьте осторожны: срез по байтам может разорвать символ посередине, если не знать точных границ. Используйте utf8.RuneCountInString() для подсчёта реального количества символов.

Также запомните: len(s) возвращает количество байтов, а не символов. Для подсчёта символов используйте utf8.RuneCountInString(s) или len([]rune(s)).