Строки в Go: работа с рунами и сортировка Unicode

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

Что такое руна и почему она важна?

  • Руна – это отдельная кодовая точка Unicode, представляемая типом rune (alias int32).
  • В отличие от byte, руна может занимать до четырёх байт в UTF‑8, что позволяет хранить любой символ мира.
  • Преобразование строки в срез рун упрощает работу с символами, потому что каждый элемент уже соответствует одному логическому символу (коду точки).

Почему сортировать строки по рунам?

Сортировка «по байтам» приводит к неверному порядку для многобайтовых символов. Например, строка "áß" в UTF‑8 представлена как [0xC3 0xA1 0xC3 0x9F]. При обычном сравнении байтов «ß» окажется раньше «á», хотя в Unicode её кодовая точка (U+00DF) больше.

Сортируя по рунам, мы сравниваем кодовые точки, а не их байтовое представление, что соответствует официальному порядку Unicode.

Как преобразовать строку в срез рун?

package main

import "fmt"

func main {
 s := "Привет, мир! 🌍"
 // Преобразуем строку в срез рун
 r := []rune(s)

 fmt.Printf("Исходная строка: %s
", s)
 fmt;Printf("Срез рун: %v
", r)
}

В результате получаем массив целых чисел, где каждый элемент – кодовая точка Unicode.

Сортировка среза рун

Для сортировки удобно использовать пакет sort. Ниже показан пример сортировки в лексикографическом порядке Unicode.

package main

import (
 "fmt"
 "sort"
)
func main {
 // Строка с разными символами
 s := "zαβγδaΩБвгд"
 // Преобразуем в руны
 r := []rune(s)

 // Сортируем с помощью sort.Slice
 sort.Slice(r, func(i, j int) bool {
 return r[i] < r[j] // сравнение кодовых точек
 })

 // Обратное преобразование в строку
 sorted := string(r)

 fmt.Printf("Исходная: %s
", s)
 fmt.Printf("Отсортированная: %s
", sorted)
}
Исходная: zαβγδaΩБвгд
Отсортированная: aБвгдzαβγδΩ

Обратное преобразование: из рун в строку

После сортировки достаточно выполнить приведение типа string(runes):

sortedString := string(runes)

Это создаст новую строку, где каждый rune будет автоматически закодирован в UTF‑8.

Подводные камни: графемы и комбинирующие знаки

Не все визуальные символы соответствуют одной руне. Некоторые «символы» состоят из нескольких кодовых точек (графемный кластер). Пример – буква é, которая может быть представлена как:

  • U+00E9 (одна руна) – «é»
  • U+0065 U+0301 (буква e + комбинирующий акут) – две руны

При сортировке по рунам такие кластеры могут попасть в разный порядок, хотя визуально они одинаковы. Чтобы правильно сортировать «по пользователю», используют нормализацию Unicode (NFC, NFD) и/или collation-библиотеки, например golang.org/x/text/collate.

Пример нормализации перед сортировкой

package main

import (
 "fmt"
 "sort"

 "golang.org/x/text/unicode/norm"
)
func main {
 words := []string{"éclair", "éclair", "apple", "banana"}

 // Приведём все строки к NFC (композитная форма)
 for i, w := range words {
 words[i] = norm.NFC.String(w)
 }

 // Сортируем обычным способом
 sort.Strings(words)

 fmt.Println(words) // => [apple banana éclair éclair]
}

После нормализации обе формы «é» становятся одинаковыми, и сортировка будет предсказуемой.

Полный практический пример

Ниже собран полностью рабочий пример, который:

  1. Считывает список строк.
  2. Нормализует их в форму NFC.
  3. Преобразует каждую строку в срез рун.
  4. Сортирует руны внутри каждой строки (полезно, например, для анаграмм).
  5. Собирает строки обратно и выводит их в отсортированном порядке.
package main

import (
 "bufio"
 "fmt"
 "os"
 "sort"

 "golang.org/x/text/unicode/norm"
)

func sortRunesInString(s string) string {
 // Нормализуем, чтобы одинаковые визуальные символы имели одинаковый набор рун
 s = norm.NFC.String(s)

 // Преобразуем в руны
 r := []rune(s)

 // Сортируем руны
 sort.Slice(r, func(i, j int) bool { return r[i] < r[j] })

 // Возвращаем строку
 return string(r)
}

func main {
 fmt.Println("Введите строки (пустая строка завершит ввод):")

 scanner := bufio.NewScanner(os.Stdin)

 var lines []string
 for scanner.Scan {
 line := scanner.Text
 if line == "" {
 break
 }
 lines = append(lines, line)
 }

 // Сортируем строки целиком (по Unicode)
 sort.Strings(lines)

 fmt.Println("
Отсортированные строки с сортировкой внутри каждой строки:")
 for _, line := range lines {
 fmt.Printf("%s → %s
", line, sortRunesInString(line))
 }}

Запуск программы:

$ go run sort_runes.go
Введите строки (пустая строка завершит ввод):
привет
мир
áß
zαβγδ

Отсортированные строки с сортировкой внутри каждой строки:
áß → ßá
мир → имр
привет → еипрвт
zαβγδ → αβγδz
  • В Go строки – это массив байтов, а rune – удобный способ работать с отдельными Unicode‑символами.
  • Для корректной сортировки необходимо преобразовать строку в срез рун, отсортировать их и собрать обратно.
  • Если в строках могут встречаться комбинирующие знаки, следует нормализовать их (NFC/NFD) перед сортировкой.
  • Для «человеческой» сортировки (collation) используйте пакет golang.org/x/text/collate или аналогичные библиотеки.

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