Avatar ·

Как получить количество символов (рун) в строке Go, а не байтов?

Всем привет! Я только начал изучать Go и столкнулся с проблемой при подсчёте символов в строке. Пишу небольшой валидатор пароля: нужно проверить, что длина пароля не меньше 8 символов. Написал такой код: ```go package main import "fmt" func main() { password := "пароль" if len(password) < 8 { fmt.Println("Пароль слишком короткий") } } ``` Но программа выводит "Пароль слишком короткий", хотя строка "пароль" состоит из 6 русских букв, а не 8. Я ожидал, что len() вернёт 6, а он возвращает 12 (если я правильно понял). Похоже, что len() считает байты, а не символы. Как правильно посчитать количество символов в строке на Go? Нужно ли как-то учитывать Unicode, если строка содержит кириллицу или эмодзи? Буду благодарен за примеры кода. Теги: строки, руны, длина строки, unicode, go <<>>

Почему len() считает байты, а не символы?

В Go строки — это неизменяемые последовательности байтов. Функция len() возвращает количество байтов, а не символов. Русские буквы, эмодзи и другие символы Unicode занимают от 1 до 4 байт в кодировке UTF-8. Например, буква "п" занимает 2 байта, поэтому строка "пароль" из 6 символов весит 12 байт.

Правильное решение: преобразование в руны

В Go тип rune представляет один символ Unicode (32-битное целое число). Чтобы получить количество символов, нужно преобразовать строку в срез рун с помощью []rune():

package main

import "fmt"

func main() {
    password := "пароль"
    charCount := len([]rune(password))
    fmt.Printf("Количество символов: %d\n", charCount) // Вывод: 6
    
    // Проверка длины пароля
    if charCount < 8 {
        fmt.Println("Пароль слишком короткий")
    } else {
        fmt.Println("Пароль подходит")
    }
}

Альтернативный подход: пакет utf8

Можно использовать функцию utf8.RuneCountInString() из стандартного пакета unicode/utf8. Этот способ эффективнее, так как не создаёт новый срез:

package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    password := "пароль"
    charCount := utf8.RuneCountInString(password)
    fmt.Printf("Количество символов: %d\n", charCount) // Вывод: 6
    
    if charCount < 8 {
        fmt.Println("Пароль слишком короткий")
    }
}

Работа с эмодзи и сложными символами

Важно: []rune() и utf8.RuneCountInString() считают кодовые точки Unicode. Некоторые эмодзи (например, семейные пары, флаги стран) состоят из нескольких кодовых точек, объединённых с помощью модификаторов. Для них эти методы могут дать некорректный результат:

package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    text := "👨‍👩‍👧‍👦" // Эмодзи "семья"
    fmt.Println("Байт:", len(text))              // 25
    fmt.Println("Рун:", utf8.RuneCountInString(text)) // 7 (каждый человек + модификаторы)
}

Для подсчёта графем (видимых символов) используйте пакет golang.org/x/text:

package main

import (
    "fmt"
    "golang.org/x/text/unicode/norm"
)

func main() {
    text := "👨‍👩‍👧‍👦"
    // Нормализация и подсчёт графем
    iter := norm.Iter{}
    iter.InitString(norm.NFC, text)
    count := 0
    for !iter.Done() {
        iter.Next()
        count++
    }
    fmt.Println("Графем:", count) // 1 (один видимый символ)
}

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

  • Для большинства случаев (английские буквы, кириллица, простые эмодзи) используйте utf8.RuneCountInString() — он быстрее и не выделяет память.
  • Если нужна высокая производительность и вы уверены, что строка содержит только ASCII, можно использовать len().
  • Для работы с текстом на естественных языках (с учётом диакритических знаков, сцепок символов) используйте пакет golang.org/x/text или библиотеку rivo/uniseg.

В вашем случае с паролем из русских букв решение с []rune() или utf8.RuneCountInString() будет работать корректно.

<<>>

Использование пакета unicode/utf8 для итерации по символам

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

package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    password := "пароль123"
    count := 0
    for i := 0; i < len(password); {
        r, size := utf8.DecodeRuneInString(password[i:])
        fmt.Printf("Символ: %c (код: %U)\n", r, r)
        count++
        i += size
    }
    fmt.Println("Всего символов:", count)
}

Этот подход позволяет одновременно подсчитать символы и получить доступ к каждому руну без создания промежуточного среза. Функция utf8.DecodeRuneInString() возвращает руну и количество байтов, которое она занимает. Это особенно полезно при работе с большими строками, когда создание среза рун может быть затратным по памяти.

Также обратите внимание на функцию utf8.RuneCount(), которая работает с срезом байтов, а не строкой — она пригодится, если у вас уже есть []byte.

Важно: все методы на основе рун корректно работают с кириллицей, иероглифами и большинством эмодзи, но не учитывают комбинированные символы (например, буква "ё" как "е" + диакритический знак). Для полной поддержки Unicode используйте нормализацию или библиотеку uniseg.