← Назад к статьям

Строки в Go

M
miholeus
18 марта 2026 г. · 5 мин чтения

Строки в Go

Строка в Go — это immutable (неизменяемая) последовательность байтов.
Важно понимать: строка — это не массив символов, а именно массив байтов. По умолчанию Go использует UTF-8 кодировку.


Внутреннее устройство

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

type stringStruct struct {
    str unsafe.Pointer  // указатель на массив байтов
    len int             // длина в байтах
}

Когда вы объявляете s := "Hello", в памяти создается:

  • Массив байтов [72 101 108 108 111] (ASCII коды)
  • Структура stringStruct, которая указывает на этот массив и хранит длину 5

Ключевые особенности

1. Неизменяемость (Immutability)

s := "hello"
s[0] = 'H'  // ОШИБКА компиляции! Нельзя изменить строку

Это фундаментальное свойство. Строки неизменяемы, что даёт:

  • Безопасность: можно передавать строки между горутинами без синхронизации
  • Эффективность: строки можно безопасно шарить (sharing), substring не копирует данные
  • Простоту: нет проблем с concurrent modification

2. UTF-8 кодирование

Go нативно поддерживает UTF-8:

s := "Привет, мир!"
fmt.Println(len(s))  // 21 (количество БАЙТОВ, не символов!)

Почему 21? Кириллические символы в UTF-8 занимают 2 байта каждый:

  • "Привет" = 6 символов × 2 байта = 12 байтов
  • ", " = 2 символа × 1 байт = 2 байта
  • "мир" = 3 символа × 2 байта = 6 байтов
  • "!" = 1 байт
  • Итого: 21 байт

3. Длина vs количество символов

s := "Hello, 世界"
// Длина в байтах
fmt.Println(len(s))  // 13
// Количество рун (символов Unicode)
fmt.Println(utf8.RuneCountInString(s))  // 9

Китайские иероглифы 世界 занимают по 3 байта каждый в UTF-8.


Работа со строками

Индексация

s := "Hello"
fmt.Println(s[0])  // 72 (ASCII код 'H')

Внимание: s[i] возвращает байт (uint8), а не символ!

Итерация

По байтам:

s := "Привет"
for i := 0; i < len(s); i++ {
    fmt.Printf("%d ", s[i])  // выведет байты
}

По рунам (правильный способ для Unicode):

s := "Привет"
for i, r := range s {
    fmt.Printf("позиция %d: символ %c (руна %d)\n", i, r, r)
}

range автоматически декодирует UTF-8 и возвращает руны (int32).

Substring (срезы строк)

s := "Hello, World!"
sub := s[7:12]  // "World"

Важная оптимизация: substring не копирует данные! Он создает новую структуру stringStruct, которая указывает на часть того же underlying массива байтов.

s := "Hello, World!"
sub := s[0:5]  // "Hello"
// sub.str указывает на тот же массив, что и s.str
// sub.len = 5

Потенциальная проблема: если вы берёте маленький substring от огромной строки и храните его долго, вся исходная строка останется в памяти.

Решение:

// Создать копию, если нужно освободить исходную строку
sub := string([]byte(s[0:5]))

Конвертация между string и []byte

string → []byte

s := "Hello"
b := []byte(s)  // КОПИРОВАНИЕ данных

Это создает копию, потому что []byte изменяемый, а string — нет.

[]byte → string

b := []byte{72, 101, 108, 108, 111}
s := string(b)  // КОПИРОВАНИЕ данных

Тоже копирование по той же причине.

Оптимизация компилятора: в некоторых случаях (например, при выводе в io.Writer) компилятор может избежать копирования:

fmt.Println(string(byteSlice))  // может избежать копирования

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

Оператор +

s := "Hello" + " " + "World"

Проблема: каждая операция + создает новую строку и копирует данные. O(n²) для цикла!

// ПЛОХО для большого количества конкатенаций
s := ""
for i := 0; i < 1000; i++ {
    s += "x"  // каждый раз копирование всей строки
}

strings.Builder (правильный способ)

var builder strings.Builder
for i := 0; i < 1000; i++ {
    builder.WriteString("x")
}
s := builder.String()

strings.Builder использует внутренний []byte буфер и минимизирует аллокации.


Руны (Runes)

Руна — это тип rune, алиас для int32, представляющий Unicode code point:

s := "世"
r := []rune(s)  // конвертация string → []rune
fmt.Println(r[0])  // 19990 (Unicode code point для 世)
r := '世'  // одинарные кавычки создают руну
fmt.Printf("%T\n", r)  // int32

Пустая строка и nil

var s1 string     // zero value, s1 == ""
s2 := ""          // то же самое
// Строки НЕ могут быть nil!
// var s3 *string = nil  // это указатель на строку, не сама строка

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

Строки сравниваются лексикографически по байтам:

"abc" < "abd"  // true
"Abc" < "abc"  // true (заглавные буквы идут раньше в ASCII/Unicode)

Сравнение происходит за O(n), но оптимизировано: сначала сравниваются длины, потом указатели (если указывают на один адрес), и только потом содержимое.


Интернирование строк (String interning)

Go автоматически переиспользует идентичные строковые литералы:

s1 := "hello"
s2 := "hello"
// s1 и s2 указывают на одну и ту же область памяти

Это работает только для литералов, не для динамически созданных строк.


Полезные пакеты

  • strings — основные операции со строками (Split, Join, Contains, Replace и т.д.)
  • strconv — конвертация между строками и другими типами
  • unicode/utf8 — работа с UTF-8 (подсчет рун, валидация)
  • regexp — регулярные выражения

Важные источники

  1. Go Blog — Strings, bytes, runes and characters in Go — отличная статья от Роба Пайка, обязательна к прочтению!
  2. Go Specification — String types
  3. Research!rsc — Go Data Structures — Расс Кокс объясняет внутреннее устройство
  4. Effective Go — Data section
  5. «The Go Programming Language» (Donovan & Kernighan) — глава 3.5

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

  1. Используйте strings.Builder для построения строк в циклах
  2. Помните про UTF-8: len(s) — это байты, не символы
  3. Используйте range для итерации по Unicode символам
  4. Избегайте конверсий string ↔ []byte в горячих местах (они копируют данные)
  5. Будьте осторожны с substring от больших строк (memory leak)

Комментарии 0

Комментарии проходят модерацию
Загрузка комментариев...