← Назад к вопросам

Как устроена строка в Go?

2.2 Middle🔥 201 комментариев
#Основы Go#Производительность и оптимизация

Комментарии (1)

🐱
gpt-5-nanoPrepBro AI4 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как устроена строка в Go

Go-проекторы часто спрашивают про строку как тип данных и как она реализована в памяти. Ниже — сжатый, но подробный разбор с примерами и практиками.

Что такое строка в Go

  • В Go строка — это тип данных, который представляет собой последовательность байтов. По умолчанию она считается закодированной в формате UTF-8, и в этом смысле строка должна содержать корректно кодированные символы Unicode.
  • Строки в Go неизменяемы (immutable). Любая операция, которая «изменяет» строку, на самом деле создает новую строку.
  • Внутренний смысл: строка — это не просто набор символов, а ссылка на последовательность байтов и её длина. Это позволяет эффективно делать срезы и копировать ссылки, но не копировать данные.

Как устроена строка в памяти

  • В реализации Go строка хранит две части: указатель на данные и длину. Это похоже на заголовок «многоячеечного» массива, но без способности изменять размер.
  • В 64-битных архитектурах строка обычно занимает две машинных слова: pointer и len. Это как бы легковесный заголовок над непрерывной областью байтов.
  • Спецификация языка не диктует точную реализацию памяти; детали зависят от реализации рантайма. Но повседневное поведение таково: строка — это ссылочная структура, данные которой могут быть общими между суб-строками, если не копировать.

Байты, руны и UTF-8

  • len(s) возвращает количество байтов, а не количество символов.
  • s[i] возвращает один байт (тип uint8). Поэтому напрямую доступ к символам может работать неправильно для многобайтовых кодовых точек.
  • Чтобы пройтись по символам Unicode (рунам), используйте range или пакет utf8:
    • range по строке итерирует по рунам.

Примеры

  • Объявление и обычная конкатенация:
package main

import "fmt"

func main() {
    s := "Привет, мир" // UTF-8 строка
    fmt.Println(s)
}
  • Доступ по байту и срезы:
package main

import "fmt"

func main() {
    s := "Привет"
    fmt.Printf("len(s) = %d\n", len(s)) // байты
    fmt.Printf("s[0] = %d ('%c')\n", s[0], s[0]) // первый байт
    sub := s[1:3] // подстрока без копирования данных
    fmt.Println(sub)
}
  • Итерация по рунам:
package main

import "fmt"

func main() {
    s := "Привет, 世界"
    for i, r := range s {
        fmt.Printf("%d: %c (%U)\n", i, r, r)
    }
}
  • Преобразования и копирование:
package main

import "fmt"

func main() {
    s := "Hello"
    b := []byte(s)   // копия байтов
    s2 := string(b)  // создаёт новую строку из тех же байтов (копия данных)
    fmt.Println(s, b, s2)
}

Raw-строки и экранирование

  • Raw строка с помощью обратных кавычек сохраняет переносы строк и не требует экранирования:
raw := `Line1
Line2\tNotEscaped`
  • Двойные кавычки поддерживают escapes:
quoted := "Line1\nLine2\tTabbed"

Производительность и практики

  • Избегайте многократной конкатенации с помощью оператора + в цикле для больших строк — это приводит к созданию новых строк на каждой итерации.
  • Для сборки больших строк используйте:
    • strings.Builder
    • bytes.Buffer (для байтовых данных)
  • Помните про эффект «задержки памяти» при срезах: срез строки s[a:b] может удерживать память всего исходного массива, если он велик. Если нужен небольшой фрагмент без удержания большого массива, стоит скопировать подстроку в новую строку:
frag := s[a:b]
copy := string([]byte(frag)) // копирование в новую строку

Важные детали

  • zero-value строки: "".
  • строки и байты — не одно и то же, Conversion между ними копирует данные.
  • При работе с Unicode рекомендуется опираться на руны и пакет utf8 для корректной обработки границ символьных единиц.

Итого: строка в Go — это immutable непрерывный массив байтов с длиной, где байты кодируют данные в UTF-8; работа с символами требует учета кодировки, а эффективная сборка строк — через Builder и разумное копирование при необходимости.