Menu

rune и byte в Golang: строки в UTF-8 простыми словами

Что означают rune и byte в Go, как строки хранят UTF-8, почему len считает байты, как range декодирует руны, преобразования между string, []byte и []rune и работа с пакетом bytes.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Строка в Go это последовательность байтов, а исходный код и строковые литералы Go в кодировке UTF-8. byte это один из этих байтов. rune это один символ Unicode (кодовая точка), который UTF-8 хранит в объёме от 1 до 4 байтов. Большая часть путаницы со строками в Go возникает, когда их смешивают:

занимает в UTF-8 три байта, поэтому в "語Go" 5 байтов, но 3 руны. Цикл по индексу видит пять байтов, и байты 0, 1 и 2 сами по себе ничего не значат. Цикл range декодирует UTF-8 и выдаёт три руны на индексах 0, 3 и 4: i всегда смещение в байтах, а не номер символа.

byte и rune это псевдонимы

ИмяТот же тип, чтоЛитералЗначение
byteuint8byte('A'), 0x41один байт данных
runeint32'A', 'é', '世'одна кодовая точка Unicode

Поскольку это псевдонимы, byte это uint8, а rune это int32; преобразование между псевдонимом и его базовым типом не нужно. Одинарные кавычки дают руну, двойные строку. 'ab' это ошибка компиляции, потому что литерал руны содержит ровно один символ.

Руны это числа, поэтому с ними можно выполнять арифметику:

'7' - '0' это классический способ превратить символ цифры в её значение. Пакет unicode классифицирует руны (IsLetter, IsDigit, IsSpace, IsUpper) и правильно меняет регистр у не-ASCII букв. %c печатает руну как символ, %U в форме U+4E16, %q как литерал руны в кавычках.

Как UTF-8 хранит символы

Кодовые точкиБайтовПримеры
от U+0000 до U+007F1ASCII: a, 7, {
от U+0080 до U+07FF2é, ß, ж, ع
от U+0800 до U+FFFF3, ,
U+10000 и выше4эмодзи, редкие иероглифы

Текст ASCII в UTF-8 не меняется, поэтому код, работающий с байтами, часто проходит тесты и ломается на первом же имени с диакритикой или кириллицей. Пакет unicode/utf8 работает с кодировкой напрямую:

% x (с пробелом) печатает байты в шестнадцатеричном виде через пробел. utf8.ValidString сообщает, является ли строка корректным UTF-8. Строки в Go могут содержать любые байты, не только корректный UTF-8; встретив некорректный байт, range выдаёт utf8.RuneError (U+FFFD, символ замены) и сдвигается на один байт.

Преобразования между string, []byte и []rune

ПреобразованиеРезультатСтоимость
[]byte(s)байты UTF-8копирование
[]rune(s)декодированные кодовые точкидекодирование и копирование (4 байта на руну)
string(b)строка из байтовкопирование
string(r), где r это []runeкодирование в UTF-8копирование
string(r), где r это runeстрока из одного символанемного

Переворот по байтам превратил бы 世界 в некорректный UTF-8, поэтому reverse работает с рунами. Но и руны это ещё не всё: символ вроде é можно записать как e плюс комбинируемый акцент, а это две руны. Для видимых пользователю символов (графемных кластеров) используйте библиотеку вроде github.com/rivo/uniseg.

Каждое преобразование копирует данные, потому что строки неизменяемы, а слайсы нет. В горячем коде не преобразуйте туда и обратно; некоторые случаи (например, string(b) в качестве ключа мапы или в сравнении) компилятор оптимизирует и копию пропускает.

Индексация даёт байт

s[i] возвращает byte, а s[i:j] режет по смещениям в байтах:

s := "café"
fmt.Println(s[3])            // 195: the first byte of é
fmt.Println(string(s[3]))    // "Ã": that byte read as a code point
fmt.Println(s[:3])           // "caf"
fmt.Println(s[:4])           // "caf\xc3": half of é, invalid UTF-8

string(s[3]) это ловушка: преобразование одного байта в string трактует его как кодовую точку 195, то есть Ã, а не как сам байт. Чтобы найти границы символов, используйте range, семейство strings.Index (они возвращают смещения в байтах, которые всегда попадают на границы) или utf8.DecodeRuneInString.

Пакет bytes

bytes повторяет strings для []byte: bytes.Contains, bytes.Split, bytes.Fields, bytes.TrimSpace, bytes.Equal, bytes.ToUpper и так далее. Используйте его, когда данные приходят байтами (содержимое файлов, тела HTTP, сетевые буферы), чтобы не преобразовывать в строку и обратно:

bytes.Buffer это растущий буфер байтов, который реализует io.Reader и io.Writer. Он подходит, когда вы и пишете, и читаете байты; для сборки строки strings.Builder чуть дешевле. Сравнивайте слайсы байтов через bytes.Equal, потому что == со слайсами не работает.

Что выбрать

  • Текст, который вы показываете, сравниваете или ищете: держите его в string и используйте range, когда идёте по символам.
  • Позиции символов, переворот, обрезка до n символов: преобразуйте в []rune.
  • Ввод-вывод, хеширование, кодирование, бинарные протоколы: []byte и пакет bytes.
  • Один символ: rune. Один сырой байт: byte.

Часто задаваемые вопросы

Что такое rune в Go?

rune это псевдоним int32, он представляет одну кодовую точку Unicode. Литерал руны пишется в одинарных кавычках: 'a' равно 97, 'é' равно 233, '世' равно 19990. Обход строки через for i, r := range s даёт руны, декодированные из байтов UTF-8.

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

byte (псевдоним uint8) это 8 бит сырых данных. rune (псевдоним int32) это целый символ Unicode. В строке UTF-8 символы ASCII занимают по одному байту, а остальные символы от двух до четырёх байтов, но всё равно остаются одной руной. len(s) считает байты; utf8.RuneCountInString(s) считает руны.

Как перебрать символы строки в Go?

Используйте for i, r := range s. Каждая итерация даёт смещение в байтах i и руну r, так что многобайтовые символы обрабатываются правильно. Обычный цикл по индексу for i := 0; i < len(s); i++ проходит по байтам и режет не-ASCII символы на куски.

Как преобразовать строку в слайс байтов в Go?

b := []byte(s) копирует байты строки в новый слайс. string(b) преобразует обратно, тоже с копированием. Если нужно работать с символами, например перевернуть строку или взять первые n символов, используйте []rune(s).

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ