Извлечение подстроки в Go: как получить часть строки по индексам?
Проблема: строки в 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).
Минусы: Не подходит, если нужно извлечь подстроку по числовым индексам символов. Медленнее для больших объёмов данных из-за поиска.
Ответов пока нет.
Войдите чтобы оставить ответ