Как строки работают в Golang внутри

В языке Go строки (string) являются неизменяемыми последовательностями байт, закодированных в UTF-8. Это означает, что строка не представляет собой последовательность символов в привычном смысле, а хранит байты, которые могут представлять как ASCII-символы (один байт), так и многобайтовые символы Unicode.

Go стремится к простоте и безопасности, поэтому строки реализованы с фокусом на неизменяемость, производительность и удобство работы с Unicode, включая поддержку рун (rune — псевдоним для int32, представляющий символ Unicode).

1. Внутреннее представление

На уровне рантайма строка в Go представлена как структура из двух полей:

type stringStruct struct {
data \*byte // указатель на первый байт строки
len int // длина строки в байтах
}

Строка — это read-only срез байтов: она указывает на участок памяти и хранит длину, но не может быть изменена без копирования.

Пример:

s := "hello"

Строка s содержит указатель на первый байт ('h') и длину 5. В памяти: [104 101 108 108 111].

2. Иммутабельность (неизменяемость)

После создания строку нельзя изменить. Любая операция, которая выглядит как изменение строки, на самом деле создаёт новую строку.

s := "hello"
// s\[0\] = 'H' // ошибка компиляции

Чтобы изменить строку, её необходимо преобразовать в срез:

b := \[\]byte(s)
b\[0\] = 'H'
s2 := string(b) // "Hello"

Иммутабельность позволяет:

  • безопасно использовать строки в многопоточности;

  • эффективно кешировать и шарить данные;

  • избежать скрытых побочных эффектов.

3. Размер строки

Строка измеряется в байтах, а не в символах. Например:

s := "Привет"
fmt.Println(len(s)) // 12 (6 символов по 2 байта в UTF-8)

Если строка состоит только из ASCII-символов, то len(s) равен количеству символов. Но при наличии Unicode-символов длина может быть больше.

4. Символы и rune

Go использует тип rune (это просто int32) для представления одного символа Unicode.

s := "你好"
fmt.Println(len(s)) // 6 (каждый иероглиф  3 байта)
fmt.Println(utf8.RuneCountInString(s)) // 2

Итерирование по строке с помощью for range возвращает rune и позицию:

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

Это безопасно для UTF-8.

5. Преобразования между строками и байтами

string → []byte

s := "hello"
b := \[\]byte(s) // копирует байты

[]byte → string

b := \[\]byte{72, 101, 108, 108, 111}
s := string(b)

Эти операции создают новые объекты в памяти. Они важны при работе с файлами, сетевыми данными и бинарными протоколами.

6. Преобразование в rune-слайс

Для безопасной работы с символами:

s := "Привет"
r := \[\]rune(s) // \[\]rune{'П','р','и','в','е','т'}

[]rune хранит каждый символ Unicode как int32, позволяя безопасно изменять строку по символам, а не байтам.

7. Сравнение строк

Операции ==, !=, <, > и другие работают побайтово (лексикографически по UTF-8):

s1 := "apple"
s2 := "banana"
fmt.Println(s1 < s2) // true

Сравнение происходит по байтам, но этого достаточно для правильного порядка Unicode-строк в большинстве случаев.

8. Конкатенация строк

Можно объединять строки через +:

s := "Hello, " + "world"

Также можно использовать strings.Join для более эффективного объединения множества строк:

parts := \[\]string{"Go", "is", "awesome"}
joined := strings.Join(parts, " ") // "Go is awesome"

Конкатенация строк создаёт новую строку. Частое использование + в цикле неэффективно.

9. Строительство строк с strings.Builder

Для эффективной по памяти и скорости сборки больших строк:

var builder strings.Builder
builder.WriteString("Hello")
builder.WriteString(", ")
builder.WriteString("world")
s := builder.String()

strings.Builder минимизирует количество аллокаций, сохраняя производительность.

10. Сравнение string и []byte

Характеристика string []byte
Изменяемость Нет Да
--- --- ---
Размер Только длина Длина + вместимость (cap)
--- --- ---
Представление UTF-8 Любые байты
--- --- ---
Используется в JSON, шаблонах, логах Файлы, сетевые соединения
--- --- ---
Подходит для Текста Бинарных данных
--- --- ---

11. Оптимизация хранения строк

Go использует оптимизированные аллокаторы, чтобы минимизировать накладные расходы на хранение строк. Иммутабельность позволяет шарить строки без копирования — например, строка, возвращаемая из json.Unmarshal, может указывать прямо на буфер с JSON-данными.

12. Работа с Unicode

Go поддерживает Unicode благодаря UTF-8. Поддерживаются:

  • unicode — проверка классов символов (буква, цифра, пунктуация и т.д.)

  • utf8 — манипуляции с кодировкой UTF-8

  • strings — стандартные функции обработки строк

Пример:

s := "å"
fmt.Println(len(s)) // 2 байта
fmt.Println(utf8.RuneCountInString(s)) // 1 rune

13. Подстроки (срезы)

Можно брать подстроки через срезы, как у слайсов:

s := "Hello, world"
sub := s\[7:\] // "world"

⚠️ Это работает по байтам, не по символам. Может обрезать символ UTF-8:

s := "Привет"
sub := s\[:5\] // ⚠️ может быть невалидной строкой

Для правильной работы с символами — используйте []rune.

14. Интернирование строк

Go не интернирует строки автоматически (в отличие от Java). Однако константы могут быть оптимизированы компилятором. При сравнении строк s1 == s2, даже если они находятся в разных местах памяти, Go сравнивает содержимое.

15. Сборка строк с помощью буферов

var buf bytes.Buffer
buf.WriteString("Go")
buf.WriteByte(' ')
buf.WriteString("Lang")
fmt.Println(buf.String()) // "Go Lang"

bytes.Buffer эффективен при работе с бинарными и текстовыми данными, но менее специализирован для строк, чем strings.Builder.

16. Особенности строк в компиляторе Go

  • Строки — не null-terminated, как в C. Они не используют \0 для завершения.

  • При передаче строк между Go и C (через cgo), необходимо вручную добавлять \0, если требуется.

  • Компилятор Go может выносить строки в readonly сегмент памяти, если они неизменны и используются как литералы.

17. Работа со строками в шаблонах, JSON и т.д.

Строки — основной способ обмена данными:

  • JSON использует map[string]interface{}.

  • HTTP-заголовки, запросы, параметры URL — всё строки.

  • В шаблонах (html/template, text/template) строки — основной носитель данных.

18. Память и эффективность

Иммутабельность и короткая структура строки (2 слова: указатель и длина) делают строки:

  • эффективными в передаче;

  • безопасными в многопоточности;

  • лёгкими в копировании (копируется только дескриптор, а не данные).

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