Avatar ·

Извлечение подстроки в Go: как получить часть строки по индексам?

Привет! Я только начал изучать Go и столкнулся с проблемой при работе со строками. В других языках (например, Python или JavaScript) я привык делать что-то вроде `str[1:4]`, чтобы получить подстроку. В Go я попробовал аналогичный синтаксис, но получил не то, что ожидал. Вот мой код: ```go package main import "fmt" func main() { str := "Привет, мир!" sub := str[1:4] fmt.Println(sub) // Ожидаю: "рив", но получаю что-то странное } ``` Вместо "рив" я вижу какие-то битые символы. Я читал, что Go использует UTF-8, но не понимаю, как это влияет на извлечение подстрок. Также я попробовал работать с ASCII-строкой: ```go str := "Hello, World!" sub := str[1:4] fmt.Println(sub) // Выводит "ell" — это работает! ``` Почему с кириллицей возникают проблемы? Как правильно извлекать подстроки из строк, содержащих Unicode-символы (русские буквы, эмодзи и т.д.)? Нужен надёжный способ, который будет работать для любых строк. Заранее спасибо! <<>>

Проблема: строки в Go — это байтовые срезы, а не последовательности символов

В Go строка — это неизменяемый срез байтов ([]byte), который интерпретируется как текст в кодировке UTF-8. Когда вы пишете str[1:4], вы берёте байты с 1-го по 3-й (не включая 4-й), а не символы. Для ASCII-символов (каждый занимает 1 байт) это совпадает с индексами символов, но для кириллицы, эмодзи и других Unicode-символов (занимающих 2+ байта) это приводит к разрыву многобайтовых последовательностей.

Пример: строка "Привет, мир!" в UTF-8 занимает 20 байт (каждая русская буква — 2 байта, пробел — 1 байт, запятая — 1 байт, "!" — 1 байт). str[1:4] берёт байты 1, 2, 3 — это середина первого символа "П" (который занимает байты 0 и 1), поэтому выводятся битые символы.

Решение 1: преобразование в срез рун (рекомендуется для простых случаев)

Руна (rune) в Go — это Unicode-кодовая точка (символ). Преобразуйте строку в []rune, работайте с индексами символов, затем преобразуйте обратно:

package main

import "fmt"

func main() {
    str := "Привет, мир!"
    runes := []rune(str) // Преобразуем в срез рун
    sub := string(runes[1:4]) // Берём руны с 1 по 3 (не включая 4)
    fmt.Println(sub) // Вывод: "рив"
}

Минусы: Создаёт новый срез рун (копирует память), что может быть неэффективно для больших строк. Не подходит для работы с графемами (например, эмодзи из нескольких рун, как флаги или скинтоны).

Решение 2: использование пакета unicode/utf8 (для продвинутых случаев)

Позволяет итерировать по рунам без копирования всей строки. Функция utf8.DecodeRuneInString декодирует первую руну из строки, возвращая её размер в байтах:

package main

import (
    "fmt"
    "unicode/utf8"
)

func SubstringByRunes(s string, start, end int) string {
    if start < 0 || start > end || end > utf8.RuneCountInString(s) {
        return ""
    }
    i := 0
    for pos := 0; pos < len(s); {
        r, size := utf8.DecodeRuneInString(s[pos:])
        if i == start {
            startByte := pos
            for i < end {
                r, size = utf8.DecodeRuneInString(s[pos:])
                pos += size
                i++
            }
            return s[startByte:pos]
        }
        pos += size
        i++
    }
    return ""
}

func main() {
    str := "Привет, мир!"
    sub := SubstringByRunes(str, 1, 4)
    fmt.Println(sub) // Вывод: "рив"
}

Плюсы: Работает напрямую с байтами исходной строки, не копирует всю строку. Эффективно для больших строк.

Решение 3: сторонние библиотеки (для сложных случаев с графемами)

Если вам нужно работать с графемами (например, эмодзи "🇷🇺" или "👨‍👩‍👧"), используйте пакет golang.org/x/text/runes или библиотеку github.com/rivo/uniseg. Пример с uniseg:

import "github.com/rivo/uniseg"

func GraphemeSubstring(s string, start, end int) string {
    gr := uniseg.NewGraphemes(s)
    var result []rune
    i := 0
    for gr.Next() {
        if i >= start && i < end {
            result = append(result, gr.Runes()...)
        }
        i++
    }
    return string(result)
}

Рекомендация

  • Для простых задач с ASCII или однобайтовыми символами — используйте []rune.
  • Для производительности на больших строках — напишите свою функцию с utf8.DecodeRuneInString.
  • Для работы с эмодзи и сложными графемами — возьмите готовую библиотеку uniseg.
<<>>

Альтернативный подход: использование пакета strings для поиска подстроки по содержанию

Если вам нужно извлечь подстроку не по индексам, а по известным маркерам (например, между двумя словами), используйте функции из пакета strings:

package main

import (
    "fmt"
    "strings"
)

func main() {
    str := "Привет, мир! Как дела?"
    // Найти подстроку между "Привет" и "мир"
    start := strings.Index(str, "Привет")
    end := strings.Index(str, "мир")
    if start != -1 && end != -1 && end > start {
        sub := str[start+len("Привет"):end]
        fmt.Println(sub) // Вывод: ", "
    }

    // Или просто получить часть после определённого слова
    after := strings.SplitN(str, "мир", 2)
    if len(after) > 1 {
        fmt.Println(after[1]) // Вывод: "! Как дела?"
    }
}

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

Минусы: Не подходит, если нужно извлечь подстроку по числовым индексам символов. Медленнее для больших объёмов данных из-за поиска.