Строки, руны и пакеты strings/strconv

Строки, байты и руны: модель UTF-8 в Go

Содержание курса

Тип rune, range по строке и декодирование UTF-8

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

Цикл for i, r := range s делает именно то, что нужно для текста: на каждом шаге он декодирует следующую UTF-8 последовательность и возвращает байтовую позицию i и кодовую точку r типа rune.

package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    s := "Привет"

    fmt.Println("len:", len(s)) // 12

    runeCount := 0
    for i, r := range s {
        fmt.Printf("i=%d r=%c (%d)\n", i, r, r)
        runeCount++
    }

    fmt.Println("итераций range:", runeCount)                        // 6
    fmt.Println("utf8.RuneCountInString:", utf8.RuneCountInString(s)) // 6
}

Вывод:

len: 12
i=0  r=П (1055)
i=2  r=р (1088)
i=4  r=и (1080)
i=6  r=в (1074)
i=8  r=е (1077)
i=10 r=т (1090)
итераций range: 6
utf8.RuneCountInString: 6

Обратите внимание: i прыгает на 2 каждый шаг, потому что каждая буква занимает два байта. Это не константа для всей кириллицы — просто так кодируются те конкретные буквы, что использованы здесь. Другие Unicode-символы могут занимать 1, 3 или 4 байта.

Для сравнения, побайтовый цикл:

byteCount := 0
for i := 0; i < len(s); i++ {
    fmt.Printf("s[%d] = %d\n", i, s[i])
    byteCount++
}
fmt.Println("байтовых итераций:", byteCount) // 12

Одна и та же строка, разное число итераций: 6 при range и 12 при побайтовом обходе. range декодирует, побайтовый цикл — нет.

Что происходит при некорректном байте? Если range встречает последовательность, которая не является валидным UTF-8, он подставляет utf8.RuneError (это константа '\uFFFD', значение 65533) и продвигает i ровно на один байт. Это поведение предсказуемо и задокументировано.

utf8.RuneCountInString(s) работает по той же логике: считает успешно декодированные руны, заменяя некорректные последовательности на RuneError и считая их за одну руну каждую. Для валидного UTF-8 результат совпадает с числом итераций range.

Важная оговорка: utf8.RuneCountInString считает кодовые точки, а не видимые символы. Один видимый знак может состоять из нескольких кодовых точек — например, при использовании комбинируемых знаков. В слове Привет каждая показанная буква представлена одной руной; в общем случае это не гарантируется.