Строки, байты и руны: модель UTF-8 в Go
Содержание курса
В предыдущих уроках вы работали со срезами []T и картами map[K]V. Строки в Go выглядят привычно, но устроены иначе — и это несоответствие между внешней простотой и внутренней моделью легко упустить. Разберём, что на самом деле хранит тип string.
string как последовательность байт: len, s[i] и отсутствие гарантии UTF-8
Тип string в Go — это неизменяемая последовательность байт. Не символов, не рун, не кодовых точек Unicode — именно байт, от 0 до 255 каждый.
Посмотрим, что происходит с кириллической строкой:
package main
import "fmt"
func main() {
s := "Привет"
fmt.Println(len(s)) // 12
fmt.Println(s[0]) // 208 (первый байт буквы 'П' в UTF-8)
fmt.Printf("%T\n", s[0]) // uint8
}
len(s) вернул 12, хотя в слове «Привет» шесть букв. Причина: каждая из этих кириллических букв кодируется двумя байтами в UTF-8, который использует исходный файл Go. Шесть букв × два байта = двенадцать. len об этом ничего не знает — он просто считает байты.
s[0] вернул 208 — это первый байт буквы «П» (0xD0). Тип результата — uint8, он же byte. Это сырой байт, не символ.
Присваивание s[0] = 'p' не скомпилируется:
cannot assign to s[0] (value of type byte)
Строка неизменяема. Индекс даёт доступ на чтение, но не на запись.
Важный нюанс: кодировка не гарантирована. Исходный файл с кириллическим литералом компилятор кодирует в UTF-8, но можно создать строку с произвольными байтами через escape-последовательности:
s2 := "\xFF\xFE\x00" // три байта, не является корректным UTF-8
fmt.Println(len(s2)) // 3
Go примет её без ошибок. Тип string не знает ни о какой кодировке — он просто держит байты. Это принципиальное решение языка: строки пригодны для хранения любых двоичных данных, не только текста.
