Язык Go (часто называют Golang) использует несколько базовых типов для работы с текстом:
- byte — один 8‑битный элемент, в Go это
uint8. - rune — 32‑битный элемент, представляющий Unicode‑кодовую точку (
int32). - string — неизменяемый массив байтов, в котором каждый байт хранит часть UTF‑8‑последовательности.
Понимание того, как эти типы взаимодействуют, критически важно при работе с многоязычными данными, эмодзи и другими нелатинскими символами.
Почему в Go нет отдельного типа «символ»?
В традиционных языках (C, Java) часто есть тип char, который хранит один символ. В Go такой тип отсутствует, потому что:
- UTF‑8 — переменной длины кодировка, где один «символ» может занимать от 1 до 4 байтов.
- В любой момент может понадобиться как доступ к отдельному байту (например, при работе с сетевыми протоколами), так и доступ к полной Unicode‑точке.
- Для представления Unicode‑точки введён тип
rune(aliasint32), а строка остаётся массивом байтов.
Строка как «срез байтов»
Строка в Go — это не массив символов, а неизменяемый срез байтов. Поэтому индексирование строки возвращает byte:
s := "Привет"
fmt.Printf("%T
", s[0]) // => uint8 (byte)
Если нам нужен именно символ, а не отдельный байт, необходимо декодировать UTF‑8‑последовательность в rune. Делается это двумя основными способами:
- Цикл
for … range— автоматически разбирает строку на руны. - Функция
utf8.DecodeRuneInStringиз пакетаunicode/utf8— позволяет вручную контролировать позицию и ширину символа.
Пример с for … range
package main
import "fmt"
func main {
s := "" // японское «привет»
for i, r := range s {
fmt.Printf("byte offset = %d, rune = %U, char = %c
", i, r, r)
}
}
byte offset = 0, rune = U+3053, char = byte offset = 3, rune = U+3093, char = byte offset = 6, rune = U+306B, char = byte offset = 9, rune = U+3061, char = byte offset = 12, rune = U+306F, char =
Пример с utf8.DecodeRuneInString
package main
import (
"fmt"
"unicode/utf8"
)
func main {
s := ""
for i := 0; i < len(s); {
r, size := utf8.DecodeRuneInString(s[i:])
fmt.Printf("byte pos = %d, rune = %U, char = %c, width = %d
",
i, r, r, size)
i += size
}
}
Оба подхода дают одинаковую последовательность рунами, что подтверждается официальной документацией пакета unicode/utf8.
Преобразования между типами
От rune к string
Самый простой способ — использовать функцию string:
r := '😀' // rune, кодовая точка U+1F600
s := string(r) // s == "😀"
От string к rune
Если строка гарантированно состоит из одного символа, можно написать:
s := "Ω"
r := []rune(s)[0] // r == 937 (U+03A9)
Для более безопасного получения первой руны лучше использовать utf8.DecodeRuneInString, которое также сообщает, сколько байтов заняло значение.
Быстрое преобразование в двоичную строку
В некоторых задачах требуется вывести двоичное представление руны. Ниже пример, использующий массив фиксированного размера, что экономит аллокации:
func RuneToBinFast(r rune) string {
if r == 0 {
return "0"
}
// 32‑битный rune → максимум 32 бита
var buf [32]byte
i := 31
for r > 0 {
if r&1 == 1 {
buf[i] = '1'
} else {
buf[i] = '0'
}
r >>= 1
i--
} return string(buf[i+1:])
}
Частые подводные камни
- Индексация строки возвращает байты, а не руны. Попытка взять
s[0]у строки, содержащей многобайтовый символ, даст только часть UTF‑8‑кода. - Длина строки (
len(s)) измеряется в байтах, а не в символах. Чтобы узнать количество руны, используйтеutf8.RuneCountInString(s)илиfor range. - Срезы строк работают с байтовыми позициями. Срез
s[1:3]может «разрезать» символ посередине, что приведёт к ошибке при последующей декодировке. - Изменяемость. Строки неизменяемы. Чтобы изменить отдельный символ, сконвертируйте строку в срез руны, измените нужный элемент и снова преобразуйте в строку:
func ReplaceAt(s string, pos int, newRune rune) string {
r := []rune(s)
if pos < 0 || pos >= len(r) {
return s // позиция вне диапазона – ничего не меняем
}
r[pos] = newRune
return string(r)
}
Практические рекомендации
- Всегда используйте
for … rangeилиutf8.DecodeRuneInString, когда вам нужен «символ», а не байт. - Для подсчёта символов в пользовательском вводе применяйте
utf8.RuneCountInString. - Если необходимо изменить строку, работайте с
[]rune, а не с[]byte, иначе рискуете испортить мультибайтовые символы. - Храните данные в UTF‑8, потому что это дефолтный формат для строк в Go и большинство внешних API ожидают именно его.
- При работе с сетевыми протоколами или бинарными форматами используйте тип
byteи явно кодируйте/декодируйте UTF‑8 при необходимости.
Сводная таблица преобразований
| Исходный тип | Целевой тип | Способ | Примечание |
|---|---|---|---|
| rune | string | string(r) | Создаёт строку из одной руны. |
| string | rune | []rune(s)[0] или utf8.DecodeRuneInString | Для многоруночных строк берите первый элемент массива руна. |
| byte | string | string([]byte{b}) | Один байт → однобайтовый символ (ASCII). |
| string | byte | s[i] | Возвращает i‑й байт, а не символ. |
| string | []rune | []rune(s) | Полное преобразование строки в массив руны. |
| []rune | string | string(runes) | Объединяет руны в UTF‑8 строку. |
Работа с текстом в Go требует чёткого понимания различий между байтами, рунами и строками. Строка — это массив байтов в кодировке UTF‑8, а rune представляет отдельный Unicode‑символ. Правильный выбор инструментов (for range, utf8.DecodeRuneInString, преобразования через []rune) позволяет писать корректный, безопасный и читаемый код, независимо от того, работаете ли вы с английским текстом, японскими кандзи или эмодзи;
Помните, что любые операции над строками, которые меняют их содержимое, требуют создания новой строки — Go гарантирует неизменяемость исходных данных, что упрощает многопоточное программирование.
Дата публикации: