Строка в 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 это псевдонимы
| Имя | Тот же тип, что | Литерал | Значение |
|---|---|---|---|
byte | uint8 | byte('A'), 0x41 | один байт данных |
rune | int32 | '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+007F | 1 | ASCII: a, 7, { |
| от U+0080 до U+07FF | 2 | é, ß, ж, ع |
| от U+0800 до U+FFFF | 3 | 語, €, 한 |
| 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).