В языке программирования Go (часто называют Golang) строки хранятся как неизменяемые последовательности байтов в кодировке UTF‑8. Иногда требуется работать с отдельными символами Unicode, а не с их байтовым представлением. Для этого в Go существует тип rune – это алиас для int32, представляющий одну Unicode‑символ (кодовую точку). В этой статье мы подробно разберём, как правильно преобразовать строку в массив (или срез) рун, какие подводные камни могут возникнуть и как оптимизировать такой процесс.
Содержание
- Зачем нужен массив рун?
- Базовое преобразование:
[]rune(s) - Ручное разборивание строки
- Работа с
utf8‑пакетом - Производительность и память
- Распространённые ошибки
- Полные примеры кода
Зачем нужен массив рун?
Строка в Go – это последовательность байтов. Если вы просто берёте len(s), вы получите количество байтов, а не количество символов. Это особенно важно, когда:
- Необходимо изменить отдельные символы строки (например, заменить букву «ё» на «е»);
- Вы хотите правильно подсчитать количество визуальных символов (графем) в тексте;
- Работаете с алгоритмами, где важен порядок символов, а не их байтовое представление (например, палиндромы, обратный порядок и т.д.).
Преобразование строки в массив рун решает эти задачи, потому что каждая руна соответствует одной Unicode‑кодовой точке.
Базовое преобразование: []rune(s)
Самый простой способ – воспользоваться встроенным преобразованием типов:
package main
import "fmt"
func main {
s := "Привет, мир! 🌍"
r := []rune(s) // массив (срез) рун
fmt.Printf("Строка: %s
", s)
fmt.Printf("Руны: %v
", r)
fmt.Printf("Количество рун: %d
", len(r))
}
Что происходит «под капотом»?
- Go сканирует строку
sот начала до конца, декодируя каждую UTF‑8‑последовательность в однуrune; - Создаётся новый срез, в котором каждый элемент – это полученная руна;
- Исходная строка остаётся неизменной (строки в Go иммутабельны).
Ручное разборивание строки
Если вам нужен более тонкий контроль (например, вы хотите пропустить определённые символы или собрать руны в отдельный буфер), можно использовать цикл for range:
func ToRuneSlice(s string) []rune {
var r []rune
for _, ch := range s {
// Здесь можно добавить любую фильтрацию, например:
// if !unicode.IsSpace(ch) { r = append(r, ch) }
r = append(r, ch)
}
return r
}
Такой подход полезен, когда требуется:
- Отфильтровать символы (пробелы, пунктуацию и т.п.);
- Заполнять массив рун заранее известным размером для экономии аллокаций.
Работа с пакетом unicode/utf8
Иногда нужно получить не только руны, но и их позицию в оригинальном байтовом массиве. Пакет unicode/utf8 предоставляет функции DecodeRuneInString и RuneCountInString:
import "unicode/utf8"
func DecodeAll(s string) []rune {
var r []rune
i := 0
for i < len(s) {
ru, size := utf8.DecodeRuneInString(s[i:])
r = append(r, ru)
i += size
}
return r
}
Эта техника позволяет:
- Отследить, сколько байтов занимает каждая руна (полезно для построения индексов);
- Обрабатывать некорректные UTF‑8‑последовательности (функция вернёт
utf8.RuneError).
Производительность и память
При работе с большими текстами (мегабайты и более) важно оценить стоимость преобразования:
| Подход | Время (O-нотация) | Доп; память | Комментарий |
|---|---|---|---|
[]rune(s) | O(n) | O(n) (новый срез рун) | Самый простой, но создает копию. |
Ручной for range с предвыделением | O(n) | O(n) | Можно заранее знать длину через utf8.RuneCountInString(s) и избежать повторных аллокаций. |
| Построчная обработка без полного копирования | O(n) | O(1) (если не сохраняете руны) | Полезно, когда нужны только отдельные операции (поиск, подсчёт). |
Пример предвыделения:
func ToRuneSliceFast(s string) []rune {
// Считаем количество рун без создания среза
runeCount := utf8.RuneCountInString(s)
r := make([]rune, runeCount) // один аллокация
i := 0
for idx, ru := range s {
r[i] = ru
i++
// idx используется только для демонстрации; его можно опустить
}
return r
}
Распространённые ошибки
- Путать длину строки и количество рун.
len(s)измеряется в байтах, аutf8.RuneCountInString(s)— в рунах. - Изменять строку через массив рун без конвертации обратно. После изменения необходимо собрать строку:
string(runes). - Не учитывать суррогатные пары. В UTF‑8 нет суррогатных пар, но в UTF‑16 они есть. Go работает с кодовыми точками, так что отдельные руны уже представляют полные символы.
- Игнорировать недопустимые последовательности. При чтении некорректных UTF‑8‑данных
utf8.DecodeRuneInStringвернётutf8.RuneErrorи размер 1 байт.
Полные примеры кода
Пример простого преобразования и обратного
package main
import (
"fmt"
)
func main {
original := "Golang 🐹 – быстрый язык"
// Преобразуем в массив рун
runes := []rune(original)
// Меняем один символ
for i, r := range runes {
if r == '🐹' {
runes[i] = '🐱' // заменяем хомяка на котика
}
}
// Собираем строку обратно
modified := string(runes)
fmt.Println("Исходная:", original)
fmt.Println("Модифицированная:", modified)
fmt.Printf("Рун: %v
", runes)
}
Поиск палиндрома в Unicode‑строке
package main
import (
"fmt"
"unicode/utf8"
)
func IsPalindrome(s string) bool {
// Быстро считаем количество рун
n := utf8.RuneCountInString(s)
if n == 0 {
return true
}
// Создаём массив рун фиксированного размера без лишних аллокаций
runes := make([]rune, n)
i := 0
for _, r := range s {
runes[i] = r
i++
}
// Проверяем симметрию
for i := 0; i < n/2; i++ {
if runes[i] != runes[n-1-i] {
return false
}
}
return true
}
func main {
fmt.Println(IsPalindrome("А роза упала на лапу Азора")) // true
fmt.Println(IsPalindrome("Привет")) // false
}
Подсчёт количества каждой руны (частотный анализ)
package main
import (
"fmt"
)
func RuneFrequency(s string) map[rune]int {
freq := make(map[rune]int)
for _, r := range s {
freq[r]++
}
return freq
}
func main {
text := "Быть или не быть, вот в чём вопрос."
freq := RuneFrequency(text)
for r, cnt := range freq {
fmt.Printf("%c : %d
", r, cnt)
}
}
Преобразование строки в массив рун – фундаментальная операция при работе с Unicode‑текстом в Go. Основные моменты, которые стоит помнить:
- Для большинства задач достаточно простого
[]rune(s); - Если требуется оптимизация памяти или предвидение количества рун, используйте
utf8.RuneCountInStringиmake([]rune, count); - Будьте внимательны к различию между байтовой длиной (
len(s)) и количеством рун; - Для обработки некорректных UTF‑8‑данных применяйте функции из пакета
unicode/utf8; - После изменения массива рун всегда собирайте строку обратно через
string(runes).
С учётом этих рекомендаций вы сможете писать надёжный и эффективный код, работающий с любыми Unicode‑строками в Go.