Чем отличается rune от byte?
Короткий ответ
byte | rune | |
|---|---|---|
| Псевдоним для | uint8 | int32 |
| Размер | 1 байт (8 бит) | 4 байта (32 бита) |
| Что представляет | один байт (число 0-255) | один символ Юникода |
| Когда использовать | сырые данные, ASCII-текст, бинарные форматы | работа с символами в любом языке |
byte — это про байты памяти, rune — это про символы.
Зачем вообще нужны разные типы
Чтобы понять разницу, нужно разобраться с тем, как хранятся текстовые данные.
Проблема с обычными байтами
Если бы один символ всегда занимал ровно один байт, никакого rune не понадобилось бы. Но один байт может хранить только 256 разных значений, а в мире существует более 150 000 символов: латиница, кириллица, китайские иероглифы, эмодзи, арабская вязь и так далее.
Решение: Юникод
Юникод — это таблица, в которой каждому символу мира присвоен свой номер (называется code point — кодовая точка):
| Символ | Code point (Юникод) |
|---|---|
A | 65 |
я | 1103 |
中 | 20013 |
🎉 | 127881 |
Чтобы хранить такие большие номера, нужно больше одного байта. Как именно записать эти номера в память — отдельный вопрос, и для него существуют разные кодировки.
Кодировка UTF-8
Go использует UTF-8 — кодировку, в которой один символ Юникода занимает от 1 до 4 байт в зависимости от того, насколько большой у него номер:
| Символ | Code point | Сколько байт в UTF-8 |
|---|---|---|
A | 65 | 1 байт |
é | 233 | 2 байта |
я | 1103 | 2 байта |
中 | 20013 | 3 байта |
🎉 | 127881 | 4 байта |
Поэтому строка в Go — это не последовательность символов, а последовательность байт, в которой символы могут занимать разное количество байт.
byte — единица памяти
byte — это просто красивое имя для uint8 (целое число от 0 до 255).
var b byte = 65
fmt.Println(b) // 65
fmt.Printf("%c\n", b) // A — символ с кодом 65 в ASCII
// эти две строки полностью эквивалентны
var b1 byte = 65
var b2 uint8 = 65Используется, когда работаешь с сырыми байтами: читаешь файл, передаёшь данные по сети, обрабатываешь бинарные форматы.
data := []byte{0x48, 0x65, 0x6C, 0x6C, 0x6F} // байты
fmt.Println(string(data)) // "Hello"rune — один символ Юникода
rune — это псевдоним для int32 (целое число до ~2 миллиардов). Одна rune хранит номер одного символа в Юникоде, и одного значения rune достаточно для любого существующего символа.
var r rune = 'я'
fmt.Println(r) // 1103 — code point
fmt.Printf("%c\n", r) // я
// эти две строки эквивалентны
var r1 rune = 1103
var r2 int32 = 1103⚠️ Обрати внимание: символ записывается через одинарные кавычки 'я', а строка — через двойные "я". Это разные вещи:
var r rune = 'я' // одна руна (число 1103)
var s string = "я" // строка (2 байта в UTF-8)Главная разница на примере
Возьмём строку с русским текстом:
s := "Привет"Как в неё «заглянуть»
Способ 1: байты (`byte`)
Если итерировать по строке как по последовательности байт, мы увидим сырые байты UTF-8, не символы:
s := "Привет"
fmt.Println(len(s)) // 12 — длина в БАЙТАХ (каждая русская буква = 2 байта)
for i := 0; i < len(s); i++ {
fmt.Printf("%d ", s[i])
}
// 208 159 209 128 208 184 208 178 208 181 209 130
// это не «буквы», это байты, которые в UTF-8 кодируют буквыs[0] — это первый байт (208), а не первая буква П. Извлечь букву через s[0] нельзя, потому что одна буква занимает 2 байта.
Способ 2: руны (`rune`)
Если использовать цикл range, Go автоматически декодирует UTF-8 и даёт нам символы:
s := "Привет"
for i, r := range s {
fmt.Printf("позиция %d: руна %d, символ %c\n", i, r, r)
}
// позиция 0: руна 1055, символ П
// позиция 2: руна 1088, символ р
// позиция 4: руна 1080, символ и
// ...Заметь: i прыгает не на 1, а на 2 — потому что каждая буква занимает 2 байта в строке.
Подсчёт количества символов
Это ключевая практическая разница:
s := "Привет"
fmt.Println(len(s)) // 12 — БАЙТ
fmt.Println(utf8.RuneCountInString(s)) // 6 — СИМВОЛОВ
fmt.Println(len([]rune(s))) // 6 — СИМВОЛОВ (преобразование в слайс рун)len(s) всегда возвращает количество байт. Чтобы узнать количество символов, нужно либо использовать utf8.RuneCountInString, либо превратить строку в слайс рун.
Преобразования
s := "Привет"
// Строка → байты (быстро, без копирования логики)
b := []byte(s) // [208 159 209 128 208 184 208 178 208 181 209 130]
fmt.Println(len(b)) // 12
// Строка → руны (медленнее, происходит декодирование UTF-8)
r := []rune(s) // [1055 1088 1080 1074 1077 1090]
fmt.Println(len(r)) // 6
// Доступ по индексу
fmt.Println(b[0]) // 208 — первый байт
fmt.Println(r[0]) // 1055 — первая руна (буква П)
fmt.Printf("%c", r[0]) // ПРеальные ошибки, которые случаются
Ошибка 1. Получить «первый символ» через s[0]
s := "Привет"
firstChar := s[0] // 208 — это не буква!
fmt.Printf("%c\n", firstChar) // мусор, потому что 208 — это половина буквы П в UTF-8Правильно:
runes := []rune(s)
firstChar := runes[0]
fmt.Printf("%c\n", firstChar) // ПОшибка 2. Обрезать строку посередине символа
s := "Привет"
trimmed := s[:3] // взять первые 3 байта
fmt.Println(trimmed) // "П�" — П (2 байта) + сломанный начальный байт буквы рПравильно:
runes := []rune(s)
trimmed := string(runes[:2])
fmt.Println(trimmed) // "Пр"Ошибка 3. Считать длину строки для пользователя
Допустим, лимит на длину никнейма — 5 символов:
nickname := "Иван"
if len(nickname) > 5 { // 8 > 5 → true, отвергнем валидное имя!
return errors.New("слишком длинное имя")
}Правильно:
import "unicode/utf8"
if utf8.RuneCountInString(nickname) > 5 {
return errors.New("слишком длинное имя")
}Когда использовать что
Используй byte (или []byte), когда:
1. Работаешь с сырыми данными
// Чтение файла
data, _ := os.ReadFile("photo.jpg") // возвращает []byte
// Сетевой протокол
buf := make([]byte, 1024)
conn.Read(buf)2. Точно знаешь, что текст — ASCII (только латиница и базовые символы)
// HTTP-заголовки, Base64, hex — всё это ASCII
auth := "Bearer abc123"
for i := 0; i < len(auth); i++ {
b := auth[i] // безопасно, каждый символ ровно 1 байт
}3. Производительность критична и не нужно понимать символы
// Подсчёт пробелов в большом файле
count := 0
for _, b := range []byte(text) {
if b == ' ' {
count++
}
}4. Работаешь с пакетом `bytes` или `[]byte` API
Многие функции в стандартной библиотеке принимают/возвращают []byte: io.Reader, crypto/sha256, encoding/json и т.д.
Используй rune (или []rune), когда:
1. Нужно работать с символами, а не байтами
// Подсчёт символов
count := utf8.RuneCountInString(s)
// Перебор символов
for _, r := range s {
fmt.Printf("%c ", r)
}2. Нужно индексировать строку по символам
runes := []rune(s)
fmt.Println(runes[5]) // шестой символ, независимо от количества байт3. Проверяешь свойства символов
import "unicode"
for _, r := range s {
if unicode.IsLetter(r) { ... }
if unicode.IsDigit(r) { ... }
if unicode.IsUpper(r) { ... }
}4. Реверсируешь строку
Самый частый пример, где byte сломается:
// ❌ Сломается на не-ASCII строках
func reverseBad(s string) string {
b := []byte(s)
for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return string(b)
}
// ✅ Корректно работает с любыми символами
func reverseGood(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}
fmt.Println(reverseBad("Привет")) // "тев�и�ир�П" — мусор
fmt.Println(reverseGood("Привет")) // "тевирП" — правильноСводная таблица
byte | rune | |
|---|---|---|
| Псевдоним | uint8 | int32 |
| Размер | 1 байт | 4 байта |
| Литерал в коде | 'A' (для ASCII) или 0x41 | 'я', '中', '🎉' |
| Что хранит | один байт (0-255) | один символ Юникода |
| Когда используется | сырые данные, ASCII | работа с текстом любого языка |
| Доступ к строке | s[i] — i-тый байт | []rune(s)[i] — i-тый символ |
| Длина строки | len(s) — число байт | utf8.RuneCountInString(s) |
| Цикл | for i := 0; i < len(s); i++ | for _, r := range s |
Главное правило для джуна
> Если работаешь с текстом и не уверен, что он только ASCII — используй `rune`. Если работаешь с бинарными данными — используй `byte`.
И помни: len(string) возвращает байты, а не символы. Это самая частая ловушка для начинающих в Go.