Golang руны в строки

Превращаем руны в строки на Golang без боли! Простые трюки и магия преобразования символов для твоего кода.

Язык Go (часто называют Golang) использует несколько базовых типов для работы с текстом:

  • byte — один 8‑битный элемент, в Go это uint8.
  • rune — 32‑битный элемент, представляющий Unicode‑кодовую точку (int32).
  • string — неизменяемый массив байтов, в котором каждый байт хранит часть UTF‑8‑последовательности.

Понимание того, как эти типы взаимодействуют, критически важно при работе с многоязычными данными, эмодзи и другими нелатинскими символами.

Почему в Go нет отдельного типа «символ»?

В традиционных языках (C, Java) часто есть тип char, который хранит один символ. В Go такой тип отсутствует, потому что:

  1. UTF‑8 — переменной длины кодировка, где один «символ» может занимать от 1 до 4 байтов.
  2. В любой момент может понадобиться как доступ к отдельному байту (например, при работе с сетевыми протоколами), так и доступ к полной Unicode‑точке.
  3. Для представления Unicode‑точки введён тип rune (alias int32), а строка остаётся массивом байтов.

Строка как «срез байтов»

Строка в Go — это не массив символов, а неизменяемый срез байтов. Поэтому индексирование строки возвращает byte:

s := "Привет"
fmt.Printf("%T
", s[0]) // => uint8 (byte)

Если нам нужен именно символ, а не отдельный байт, необходимо декодировать UTF‑8‑последовательность в rune. Делается это двумя основными способами:

  • Цикл for … range — автоматически разбирает строку на руны.
  • Функция utf8.DecodeRuneInString из пакета unicode/utf8 — позволяет вручную контролировать позицию и ширину символа.

Пример с for … range

package main

import "fmt"

func main {
 s := "" // японское «привет»
 for i, r := range s {
 fmt.Printf("byte offset = %d, rune = %U, char = %c
", i, r, r)
 }
}
byte offset = 0, rune = U+3053, char = 
byte offset = 3, rune = U+3093, char = 
byte offset = 6, rune = U+306B, char = 
byte offset = 9, rune = U+3061, char = 
byte offset = 12, rune = U+306F, char = 

Пример с utf8.DecodeRuneInString

package main

import (
 "fmt"
 "unicode/utf8"
)

func main {
 s := ""
 for i := 0; i < len(s); {
 r, size := utf8.DecodeRuneInString(s[i:])
 fmt.Printf("byte pos = %d, rune = %U, char = %c, width = %d
",
 i, r, r, size)
 i += size
 }
}

Оба подхода дают одинаковую последовательность рунами, что подтверждается официальной документацией пакета unicode/utf8.

Преобразования между типами

От rune к string

Самый простой способ — использовать функцию string:

r := '😀' // rune, кодовая точка U+1F600
s := string(r) // s == "😀"

От string к rune

Если строка гарантированно состоит из одного символа, можно написать:

s := "Ω"
r := []rune(s)[0] // r == 937 (U+03A9)

Для более безопасного получения первой руны лучше использовать utf8.DecodeRuneInString, которое также сообщает, сколько байтов заняло значение.

Быстрое преобразование в двоичную строку

В некоторых задачах требуется вывести двоичное представление руны. Ниже пример, использующий массив фиксированного размера, что экономит аллокации:

func RuneToBinFast(r rune) string {
 if r == 0 {
 return "0"
 }
 // 32‑битный rune → максимум 32 бита
 var buf [32]byte
 i := 31
 for r > 0 {
 if r&1 == 1 {
 buf[i] = '1'
 } else {
 buf[i] = '0'
 }
 r >>= 1
 i--
 } return string(buf[i+1:])
}

Частые подводные камни

  • Индексация строки возвращает байты, а не руны. Попытка взять s[0] у строки, содержащей многобайтовый символ, даст только часть UTF‑8‑кода.
  • Длина строки (len(s)) измеряется в байтах, а не в символах. Чтобы узнать количество руны, используйте utf8.RuneCountInString(s) или for range.
  • Срезы строк работают с байтовыми позициями. Срез s[1:3] может «разрезать» символ посередине, что приведёт к ошибке при последующей декодировке.
  • Изменяемость. Строки неизменяемы. Чтобы изменить отдельный символ, сконвертируйте строку в срез руны, измените нужный элемент и снова преобразуйте в строку:
func ReplaceAt(s string, pos int, newRune rune) string {
 r := []rune(s)
 if pos < 0 || pos >= len(r) {
 return s // позиция вне диапазона – ничего не меняем
 }
 r[pos] = newRune
 return string(r)
}

Практические рекомендации

  1. Всегда используйте for … range или utf8.DecodeRuneInString, когда вам нужен «символ», а не байт.
  2. Для подсчёта символов в пользовательском вводе применяйте utf8.RuneCountInString.
  3. Если необходимо изменить строку, работайте с []rune, а не с []byte, иначе рискуете испортить мультибайтовые символы.
  4. Храните данные в UTF‑8, потому что это дефолтный формат для строк в Go и большинство внешних API ожидают именно его.
  5. При работе с сетевыми протоколами или бинарными форматами используйте тип byte и явно кодируйте/декодируйте UTF‑8 при необходимости.

Сводная таблица преобразований

Исходный тип Целевой тип Способ Примечание
rune string string(r) Создаёт строку из одной руны.
string rune []rune(s)[0] или utf8.DecodeRuneInString Для многоруночных строк берите первый элемент массива руна.
byte string string([]byte{b}) Один байт → однобайтовый символ (ASCII).
string byte s[i] Возвращает i‑й байт, а не символ.
string []rune []rune(s) Полное преобразование строки в массив руны.
[]rune string string(runes) Объединяет руны в UTF‑8 строку.

Работа с текстом в Go требует чёткого понимания различий между байтами, рунами и строками. Строка — это массив байтов в кодировке UTF‑8, а rune представляет отдельный Unicode‑символ. Правильный выбор инструментов (for range, utf8.DecodeRuneInString, преобразования через []rune) позволяет писать корректный, безопасный и читаемый код, независимо от того, работаете ли вы с английским текстом, японскими кандзи или эмодзи;

Помните, что любые операции над строками, которые меняют их содержимое, требуют создания новой строки — Go гарантирует неизменяемость исходных данных, что упрощает многопоточное программирование.


Дата публикации: