Строки, байты и руны: модель UTF-8 в Go
Содержание курса
Преобразование string ↔ []byte и string ↔ []rune
Иногда нужно получить изменяемую копию строки — чтобы модифицировать отдельные байты или работать с рунами по индексу. Для этого есть два явных преобразования.
[]byte(s) создаёт срез, где каждый элемент — один байт исходной строки. []rune(s) декодирует строку как UTF-8 и возвращает срез кодовых точек — каждый элемент это rune.
package main
import "fmt"
func main() {
s := "Привет"
b := []byte(s)
r := []rune(s)
fmt.Println(len(s)) // 12 — байты
fmt.Println(len(b)) // 12 — байты
fmt.Println(len(r)) // 6 — руны
}
Почему len(r) == 6, а len(s) == 12? []rune(s) декодировал 12 байт в 6 кодовых точек. Каждый элемент среза — int32, и len считает элементы, а не байты.
Оба преобразования создают копию данных. Изменение среза не затрагивает исходную строку — это отдельная память.
Изменение через []byte: работает только с ASCII
Модифицировать []byte кириллической строки побайтово опасно. Буква «П» кодируется двумя байтами: D0 9F. Если заменить только первый байт на ASCII-символ, второй байт (9F) остаётся на месте как одиночный continuation byte — строка станет некорректным UTF-8:
b := []byte("Привет")
b[0] = 'X' // заменяет D0 на X, оставляет 9F — битый UTF-8
fmt.Println(string(b)) // отображение зависит от среды; знак замены или мусор
Для демонстрации независимости копии безопаснее использовать ASCII-строку:
package main
import "fmt"
func main() {
s := "Hello"
b := []byte(s)
b[0] = 'J'
fmt.Println(s) // Hello — не изменилась
fmt.Println(string(b)) // Jello — только срез
}
Изменение символа через []rune — правильный способ
Если нужно заменить конкретный символ в кириллическом тексте, используйте []rune. Каждый элемент — целая кодовая точка, поэтому замена одного элемента корректна:
package main
import "fmt"
func main() {
s := "Привет"
r := []rune(s)
r[0] = 'X'
fmt.Println(string(r)) // Xривет — корректный результат
}
Обратные преобразования: string(b) строит строку из []byte, string(r) — из []rune, кодируя каждую руну в UTF-8.
Подсчёт рун через []rune
Длина среза []rune(s) — это число декодированных кодовых точек. Можно написать функцию подсчёта рун без импорта unicode/utf8:
func countRunes(s string) int {
return len([]rune(s))
}
func main() {
s := "Привет"
fmt.Println(len(s)) // 12
fmt.Println(countRunes(s)) // 6
}
len([]rune(s)) и utf8.RuneCountInString(s) дают одинаковый результат для любой строки — в том числе с некорректными байтами — потому что оба следуют одной семантике: каждый некорректный байт считается за одну руну (RuneError). Разницы нет ни для валидного UTF-8, ни для произвольных байтов.
Практическое правило: если нужно только посчитать руны, используйте utf8.RuneCountInString — он не выделяет срез. []rune(s) создавайте тогда, когда сам срез нужен для работы: доступа по индексу, изменения элементов или передачи в функцию.
Индексирование по []rune решает задачу, которую не решает s[i]: r[3] даст четвёртую кодовую точку строки, а s[3] — просто четвёртый байт, который в середине кириллической строки окажется продолжением многобайтовой последовательности, а не самостоятельным символом.
