Skip to main content

Чем отличается rune от byte?

Короткий ответ

byterune
Псевдоним дляuint8int32
Размер1 байт (8 бит)4 байта (32 бита)
Что представляетодин байт (число 0-255)один символ Юникода
Когда использоватьсырые данные, ASCII-текст, бинарные форматыработа с символами в любом языке

byte — это про байты памяти, rune — это про символы.

Зачем вообще нужны разные типы

Чтобы понять разницу, нужно разобраться с тем, как хранятся текстовые данные.

Проблема с обычными байтами

Если бы один символ всегда занимал ровно один байт, никакого rune не понадобилось бы. Но один байт может хранить только 256 разных значений, а в мире существует более 150 000 символов: латиница, кириллица, китайские иероглифы, эмодзи, арабская вязь и так далее.

Решение: Юникод

Юникод — это таблица, в которой каждому символу мира присвоен свой номер (называется code point — кодовая точка):

СимволCode point (Юникод)
A65
я1103
20013
🎉127881

Чтобы хранить такие большие номера, нужно больше одного байта. Как именно записать эти номера в память — отдельный вопрос, и для него существуют разные кодировки.

Кодировка UTF-8

Go использует UTF-8 — кодировку, в которой один символ Юникода занимает от 1 до 4 байт в зависимости от того, насколько большой у него номер:

СимволCode pointСколько байт в UTF-8
A651 байт
é2332 байта
я11032 байта
200133 байта
🎉1278814 байта

Поэтому строка в 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("Привет"))  // "тевирП" — правильно

Сводная таблица

byterune
Псевдонимuint8int32
Размер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.