Golang строку в массив рун

Узнайте, как в Go быстро и безболезненно превратить строку в массив рун, разобраться с юникодом, увидеть примеры кода и типичные подводные камни.

В языке программирования Go (часто называют Golang) строки хранятся как неизменяемые последовательности байтов в кодировке UTF‑8. Иногда требуется работать с отдельными символами Unicode, а не с их байтовым представлением. Для этого в Go существует тип rune – это алиас для int32, представляющий одну Unicode‑символ (кодовую точку). В этой статье мы подробно разберём, как правильно преобразовать строку в массив (или срез) рун, какие подводные камни могут возникнуть и как оптимизировать такой процесс.


Содержание

  1. Зачем нужен массив рун?
  2. Базовое преобразование: []rune(s)
  3. Ручное разборивание строки
  4. Работа с utf8‑пакетом
  5. Производительность и память
  6. Распространённые ошибки
  7. Полные примеры кода

Зачем нужен массив рун?

Строка в 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.