Avatar ·

Как перевернуть строку в Go: обратный порядок символов

Всем привет! Я только начал изучать Go и столкнулся с простой, на первый взгляд, задачей — перевернуть строку. В Python я бы написал `'hello'[::-1]` и получил бы `'olleh'`. В Go я попробовал сделать так: ```go package main import "fmt" func reverse(s string) string { var result string for i := len(s) - 1; i >= 0; i-- { result += string(s[i]) } return result } func main() { fmt.Println(reverse("hello")) // Выводит: olleh } ``` С ASCII-строкой вроде "hello" это работает. Но когда я попробовал перевернуть русское слово "привет" или строку с эмодзи, например "👋Привет", я получил полную абракадабру. Для "привет" выводится что-то вроде "тверип" — вроде похоже, но не совсем правильно, а для эмодзи вообще выдаёт кракозябры. Я понимаю, что проблема, скорее всего, в кодировке. Go использует UTF-8, и моя простая итерация по байтам не работает для многобайтовых символов. Но как правильно написать функцию реверса строки в Go? Нужно ли преобразовывать строку в слайс рун? Или есть какой-то встроенный способ? Буду благодарен за подробное объяснение с рабочим примером. <<>>

Проблема байтовой итерации

Ваш первый подход не работает для не-ASCII символов, потому что Go хранит строки как последовательность байтов в кодировке UTF-8. Один символ (руна) может занимать от 1 до 4 байтов. Когда вы итерируете по байтам, вы разбиваете многобайтовые символы на отдельные байты, что приводит к некорректному результату.

Правильное решение: работа с рунами

В Go есть встроенный тип rune — это алиас для int32, представляющий один Unicode-символ. Чтобы корректно перевернуть строку, нужно сначала преобразовать её в слайс рун, затем перевернуть слайс, и только потом собрать обратно в строку.

package main

import "fmt"

func reverseRunes(s string) string {
    // Преобразуем строку в слайс рун
    runes := []rune(s)
    
    // Переворачиваем слайс
    for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
        runes[i], runes[j] = runes[j], runes[i]
    }
    
    // Преобразуем обратно в строку
    return string(runes)
}

func main() {
    tests := []string{
        "hello",
        "привет",
        "👋Привет",
        "世界",
        "a b c",
    }
    
    for _, s := range tests {
        fmt.Printf("%q -> %q\n", s, reverseRunes(s))
    }
}

Вывод программы:

"hello" -> "olleh"
"привет" -> "тевирп"
"👋Привет" -> "тевирП👋"
"世界" -> "界世"
"a b c" -> "c b a"

Как это работает?

  1. Преобразование в руны: []rune(s) корректно декодирует UTF-8 последовательность, создавая слайс, где каждый элемент — это один символ, независимо от количества байтов.
  2. Реверс слайса: Классический алгоритм с двумя указателями (i — с начала, j — с конца), меняющими элементы местами до встречи посередине.
  3. Обратное преобразование: string(runes) кодирует руны обратно в UTF-8 строку.

Важные нюансы

  • Производительность: Преобразование в руны создаёт новый слайс (аллокация памяти), что может быть затратно для очень больших строк. Для большинства случаев это приемлемо.
  • Графемные кластеры: Даже работа с рунами не идеальна для всех случаев. Например, флаг "🇷🇺" состоит из двух рун (региональных индикаторов). Реверс по рунам разобьёт флаг на части. Для полной поддержки Unicode нужна библиотека golang.org/x/text.
  • Изменяемость: Строки в Go неизменяемы, поэтому мы работаем с копией данных через слайс рун.

Альтернатива с bytes.Buffer (менее эффективно)

Можно использовать bytes.Buffer для построения результата:

func reverseBuffer(s string) string {
    var buf bytes.Buffer
    runes := []rune(s)
    for i := len(runes) - 1; i >= 0; i-- {
        buf.WriteRune(runes[i])
    }
    return buf.String()
}

Этот вариант менее эффективен из-за дополнительных операций записи в буфер, но может быть полезен для потоковой обработки.

Итог

Для корректного реверса строки в Go всегда используйте преобразование в []rune. Это стандартный идиоматичный подход, который работает для всех Unicode-символов, кроме составных графемных кластеров. Если вам нужна работа с текстом на уровне человеческого восприятия (например, для UI), рассмотрите библиотеки для работы с графемами.

<<>>

Реверсирование без создания нового слайса рун

Если вы хотите избежать аллокации памяти под слайс рун (например, для оптимизации работы с очень большими строками), можно использовать пакет unicode/utf8 для итерации по рунам напрямую:

import "unicode/utf8"

func reverseUTF8(s string) string {
    // Сначала считаем количество рун
    size := utf8.RuneCountInString(s)
    if size <= 1 {
        return s
    }
    
    // Создаём слайс байтов нужного размера
    result := make([]byte, len(s))
    pos := len(result)
    
    for len(s) > 0 {
        r, size := utf8.DecodeRuneInString(s)
        pos -= size
        utf8.EncodeRune(result[pos:], r)
        s = s[size:]
    }
    
    return string(result)
}

Как это работает?

Мы используем utf8.DecodeRuneInString для извлечения первой руны из строки, а затем utf8.EncodeRune для записи её в конец результирующего байтового слайса. Таким образом, мы обходимся без промежуточного слайса рун, работая напрямую с байтами.

Когда это нужно?

  • При работе с очень длинными строками (сотни мегабайт), где двойная аллокация памяти критична.
  • В высоконагруженных системах, где важна каждая микросекунда.
  • При обработке потоковых данных, когда строка не помещается в память целиком.

Однако для 99% случаев простой подход с []rune является более читаемым и предпочтительным. Оптимизация нужна только если профилирование показало, что реверс строк является узким местом.