Сегмент constants в Go - устройство constants-segment и практическое использование

19 февраля 2026
Автор

Олег Марков

Введение

Сегмент constants (часто называемый constants-segment или сегмент констант) — это часть скомпилированного бинарника, в которую компилятор Go складывает значения констант и других неизменяемых данных. Вы его не видите напрямую в коде, но он напрямую влияет на:

  • структуру исполняемого файла;
  • распределение памяти процесса;
  • производительность при работе с константами и литералами;
  • размер бинарника.

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

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

  • тем, кто оптимизирует производительность и память;
  • тем, кто пишет библиотеки низкого уровня;
  • тем, кто изучает внутреннее устройство Go и его рантайма.

Что такое constants-segment в Go

Логическое и физическое понятие сегмента констант

Когда говорят о сегменте constants в контексте Go, обычно имеют в виду:

  • логический уровень — область в памяти процесса, где лежат константы и неизменяемые данные (аналог .rodata, .text и других секций ELF / PE / Mach-O);
  • уровень Go-рантайма — набор указателей и границ (rodata, noptrdata, funcdata и т. д.), с которыми работает сборщик мусора (GC) и другие части рантайма;
  • уровень компилятора — результат работы cmd/compile, который группирует константы и литералы в общую область, чтобы:
    • не дублировать одинаковые значения;
    • упростить адресацию;
    • гарантировать неизменяемость данных.

Физически в бинарнике это обычно несколько секций:

  • .rodata — основная область неизменяемых данных;
  • дополнительные секции, которые рантайм использует для картирования (mapping) и маркировки указателей / неуказателей.

Какие данные попадают в constants-segment

Давайте перечислим основные типы данных, которые оказываются в области констант:

  1. Числовые константы, которые:

    • либо используются в выражениях и оптимизируются до немедленных значений (immediates) в инструкциях;
    • либо превращаются в значения в .rodata, если:
      • нужны как адресуемые данные (например, часть таблицы);
      • используются в слайсах/массивах/структурах, которые имеют статическую инициализацию.
  2. Строковые литералы:

    • всегда живут в области только для чтения;
    • разные вхождения одинаковой строки обычно интернируются (поделятся одним и тем же хранилищем).
  3. Константные массивы / структуры, инициализируемые литералами:

    • если вы объявляете глобальный массив и инициализируете его известными на этапе компиляции значениями, то сами значения обычно попадают в сегмент констант.
  4. Метаданные компилятора и рантайма:

    • имена типов;
    • имена методов и полей;
    • имена пакетов;
    • информация для отражения (reflection);
    • информация для паник, трассировок стека и т. д.

Часть этих данных логически относится к константной области, хотя в бинарнике может быть разбита по нескольким секциям.

Почему constants-segment важен для разработчика

На первый взгляд, это внутренняя деталь реализации. Но она влияет на:

  • размер бинарника — большие таблицы констант, большие строковые литералы, подготовленные карты (maps) и массивы;
  • потребление памяти — все, что лежит в сегменте констант, попадает в адресное пространство процесса и частично в рабочий набор памяти;
  • работу GC — константные области либо не сканируются, либо сканируются по особым правилам, что влияет на время пауз и общее время работы сборщика;
  • возможности оптимизации — некоторые операции компилятор может заменить доступом к константе, а некоторые — нет.

Теперь давайте посмотрим, как ваш код превращается в данные в сегменте констант.

Как Go компилирует константы в сегмент constants

Пример: числовые и строковые константы

Возьмем простой пример. Здесь я размещаю пример, чтобы вам было проще понять:

package main

const Pi = 3.1415926535

const (
    MaxUsers   = 1000
    ServerName = "prod-server-1"
)

var greeting = "Hello, constants segment"

func main() {
    // Используем константы и строку,
    // чтобы компилятор не удалил их как неиспользуемые
    _ = Pi + MaxUsers
    println(ServerName, greeting)
}

Разберем, что происходит при компиляции:

  • Pi, MaxUsers и ServerNameкомпиляторные константы:

    • при подстановке в выражения они часто не требуют выделения памяти вообще;
    • значение может быть встроено напрямую в инструкции процессора.
  • ServerName при этом еще и строковый литерал:

    • его содержимое "prod-server-1" попадает в сегмент констант (обычно .rodata);
    • структура string в Go — это пара: указатель на данные и длина;
    • сама структура, если используется как глобальная переменная, может попасть в .data или rodata, а байты строки — в rodata.
  • greeting — это глобальная переменная, инициализируемая строковым литералом:

    • байты строки "Hello, constants segment" — в константной области;
    • сама переменная greeting как string — в области глобальных данных (обычно .data), но указывает на неизменяемые байты в rodata.

Немедленные значения против данных в constants-segment

Важно понимать разницу:

  • немедленные значения (immediate) — константа вшита в инструкцию:

    • например, MOVQ $42, AX;
    • память в сегменте констант не нужна для хранения 42.
  • адресуемые константы — когда нужна ссылка (указатель) на значение:

    • элемент таблицы;
    • часть массива или структуры;
    • литерал, который передается как адресуемый объект;
    • в таком случае значение размещается в сегменте констант, а код работает с его адресом.

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

package main

// Таблица коэффициентов - известна на этапе компиляции
var coeffs = [...]float64{
    0.1, 0.2, 0.3, 0.4,
}

// Используем для вычислений
func SumCoeffs() float64 {
    var sum float64
    for _, c := range coeffs {
        sum += c
    }
    return sum
}

Что здесь попадает в сегмент констант:

  • массив coeffs — это глобальные данные;
  • но его содержимое — это набор значений float64, вычисленных на этапе компиляции;
  • компилятор формирует статический блок данных в константной области (или в .data с флагом только чтение, в зависимости от платформы и опций сборки);
  • рантайм получает фиксированный массив констант, который не меняется в ходе работы.

Интернирование строковых констант

Go-компилятор, как правило, не дублирует одинаковые строковые литералы. Если вы много раз пишете одну и ту же строку, она будет храниться в сегменте констант один раз, а разные места кода будут указывать на один и тот же набор байт.

Пример:

package main

func A() string {
    // Строковый литерал "OK"
    return "OK"
}

func B() string {
    // Тот же самый строковый литерал
    return "OK"
}

Как видите, этот код возвращает одинаковые строки. В большинстве случаев:

  • байтовое содержимое "OK" будет одно;
  • структуры string, которые создаются на стеке при возврате, будут ссылаться на одни и те же байты в сегменте констант.

Это косвенно уменьшает размер бинарника и экономит память.

Взаимодействие рантайма Go с сегментом constants

Карта памяти и роль .rodata

Рантайм Go делит память программы на несколько крупных областей:

  • код (text);
  • неизменяемые данные (rodata, funcdata и т. д.);
  • глобальные и статические данные (data, bss, noptrdata, noptrbss);
  • кучи (heap);
  • стеки горутин.

Сегмент constants — это в первую очередь часть rodata и связанных секций. Для рантайма важно знать:

  • где начинаются и где заканчиваются эти области;
  • какие из них содержат указатели, а какие — нет (например, noptrdata).

Сборщик мусора должен понимать, какие участки памяти нужно сканировать в поисках указателей на кучу, а какие можно пропустить. Многие данные в constants-segment не содержат указателей (например, массив чисел), и это ускоряет GC.

Маркировка областей без указателей

Go-компилятор и линкер делят статические данные на:

  • обычные данные (могут содержать указатели);
  • данные без указателей (noptrdata и noptrbss).

Данные, помеченные как noptr, могут находиться в области, которую GC вообще не сканирует:

  • это экономит время;
  • снижает накладные расходы на сборку мусора.

Если ваш большой массив констант не содержит ссылок на кучу (например, массив float64), он может быть размещен именно в такой области.

Константы, к которым обращается GC

Некоторые константные структуры все-таки могут содержать указатели:

  • например, глобальные таблицы с указателями на функции;
  • структуры для рефлексии (содержат указатели на типы и методы);
  • таблицы интерфейсов.

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

Теперь давайте посмотрим, как это может проявиться в вашем коде.

Статические инициализации и constants-segment

Статически инициализируемые переменные

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

Пример:

package main

// Полностью статически инициализируемый массив
var lookup = [5]int{10, 20, 30, 40, 50}

// Нельзя полностью вычислить на этапе компиляции
var runtimeInit = []int{len(lookup), 2, 3}

func main() {
    _ = lookup
    _ = runtimeInit
}

Что происходит:

  • lookup:

    • известен полностью на этапе компиляции;
    • компилятор просто кладет эти 5 чисел в статический блок данных (частично в константную область или в обычные данные, в зависимости от контекста).
  • runtimeInit:

    • первый элемент — len(lookup), а это уже выражение, зависящее от другого объекта;
    • для слайса нужна динамическая структура: указатель, длина, емкость;
    • итог — инициализация происходит при старте программы через сгенерированный код, а не чисто через constants-segment.

Большие таблицы и влияние на бинарник

Если вы создаете большие таблицы констант, например:

package main

// Воображаемая таблица синусов на 360 градусов
var sinTable = [360]float64{
    0.0, 0.0174524, 0.0348995, /* ... и так далее ... */ 0.0,
}

func main() {
    _ = sinTable
}

Такая таблица:

  • увеличит размер бинарника;
  • попадет в область статических данных, большая часть которых неизменяема;
  • будет загружена в адресное пространство процесса.

Иногда выгоднее:

  • генерировать таблицу при старте программы (если это не критично по времени);
  • либо хранить ее в меньшем формате (например, float32) и пересчитывать до float64 по мере необходимости.

Константы, строки и управление памятью

Строковые литералы и их жизненный цикл

Строковые литералы в Go:

  • живут столько же, сколько приложение;
  • не перемещаются, не освобождаются и не модифицируются;
  • доступны по стабильным адресам.

Покажу вам, как это реализовано на практике:

package main

func GetMessage() string {
    // Строка живет в сегменте констант
    return "precomputed-message"
}

func main() {
    msg1 := GetMessage()
    msg2 := GetMessage()

    // Эти две строки указывают на одинаковые байты в rodata
    println(msg1 == msg2) // true
}

Внутри:

  • структура string (data *byte, len int) может копироваться;
  • указатель data всегда ведет в константную область;
  • GC знает, что эта память не на куче, и не пытается ее перемещать или освобождать.

Константные слайсы и массивы

Иногда хочется объявить "константный слайс". В Go const можно применить только к простым типам (числа, строки, булевы значения), поэтому обычно используют глобальные var:

package main

// Спрайты, таблица значений или константные параметры
var DefaultPorts = []int{80, 443, 8080}

func main() {
    _ = DefaultPorts
}

Что здесь:

  • слайс DefaultPorts — изменяемый объект в памяти (указатель, длина, емкость);
  • сами элементы [80, 443, 8080] — значения, которые известны во время компиляции.

Возможные варианты размещения:

  • значения [80, 443, 8080] могут быть зашиты в статический блок данных (ту же константную область);
  • структура слайса может быть инициализирована при старте через сгенерированный код.

Если вы хотите гарантировать невозможность изменения, используйте:

  • экспортируемую функцию, возвращающую копию или новый слайс;
  • или возвращайте массив через указатель / обертку.

Константы и аллокации на куче

Важно понимать: сами по себе константы в сегменте constants не вызывают аллокации на куче. Но операции над ними могут:

package main

func MakeSlice() []int {
    // Литерал массива инициализируется в статике
    a := [3]int{1, 2, 3}

    // Создается слайс, который ссылается на a
    return a[:]
}

Здесь:

  • массив a лежит на стеке (обычно, он маленький);
  • слайс, возвращаемый из функции, указывает на память стека;
  • но стека после выхода из функции не будет, поэтому компилятор может:
    • промотировать a на кучу;
    • или оптимизировать этот код.

Сегмент констант здесь не участвует. Но если вы используете глобальные массивы/таблицы:

var table = [...]int{1, 2, 3, 4, 5}

func SliceFromTable() []int {
    // Слайс указывает на глобальный массив, лежащий в статике
    return table[:3]
}

Теперь:

  • table лежит в статических данных, которые заданы на этапе компиляции (их значения попадают в сегмент констант или аналогичную область);
  • слайс не приводит к аллокации на куче, он просто указывает на эту статическую память.

Это важный паттерн, когда вы хотите избежать лишних аллокаций.

Влияние constants-segment на производительность и размер

Когда константы помогают производительности

Константы и сегмент constants помогают:

  1. Избежать аллокаций:

    • строки-литералы и глобальные массивы не требуют аллокаций в куче;
    • доступ к ним выполняется через фиксированные адреса.
  2. Упростить работу GC:

    • области без указателей не сканируются;
    • неизменяемые данные не нуждаются в дополнительном учете.
  3. Ускорить доступ к данным:

    • если данные локализованы и к ним много обращений, это хорошо ложится в кэш процессора.

Когда константы ухудшают ситуацию

Есть и обратная сторона:

  1. Раздувание бинарника:

    • большие таблицы, длинные строки, заранее сгенерированные структуры — всё это увеличивает размер файла;
    • это влияет на:
      • время загрузки;
      • потребление диска;
      • передачу бинарников по сети.
  2. Память процесса:

    • даже если вы не используете все константы сразу, они занимают адресное пространство;
    • часть данных может подгружаться по требованию (demand paging), но крупные области rodata всё равно влияют на общий footprint.
  3. Кэш процессора:

    • если вы часто бегаете по огромной таблице, вы можете вытеснять из кэша другие полезные данные;
    • это не столько проблема сегмента констант, сколько проблема общего доступа к памяти.

Практические советы по использованию констант

Давайте подведем промежуточные рекомендации:

  1. Используйте строковые литералы вместо создания новых строк:

    • не делайте string([]byte("some")), если вам просто нужна строка;
    • строковый литерал — это один объект в сегменте констант.
  2. Глобальные константные таблицы — это нормально, но разумно:

    • таблица на 1–10 тысяч элементов обычно не проблема;
    • гигантские таблицы на мегабайты стоит оценивать критичнее.
  3. Если таблица редко используется, подумайте о ленивой инициализации:

    • можно хранить сжатые данные и распаковывать по требованию;
    • или генерировать таблицу в рантайме.
  4. Не бойтесь числа констант, бойтесь их объема:

    • десятки тысяч мелких целочисленных констант не опасны, если они не материализуются в виде таблиц;
    • важно, сколько байт реально попадает в .rodata.

Как увидеть и исследовать constants-segment на практике

Использование go tool compile и go tool objdump

Вы можете заглянуть внутрь бинарника и посмотреть, как компилятор разложил ваши константы.

Пример для Linux / macOS:

# Скомпилируем программу с сохранением промежуточных файлов
go build -gcflags="-S" -o app main.go

Флаг -S выводит ассемблерный листинг, где вы увидите:

  • обращения к строкам через символы вроде go.string.*;
  • константы инициализации глобальных массивов.

Для анализа готового бинарника удобно использовать go tool objdump:

go tool objdump -s "main\." ./app

Комментарии в листинге покажут, какие символы относятся к константам и какие обращения к ним происходят.

Использование внешних инструментов (objdump, nm)

Можно также использовать системные инструменты:

# Для ELF (Linux)
objdump -h app      # показывает секции (включая .rodata)
objdump -s -j .rodata app  # дамп сегмента констант

nm -S app           # список символов и их размеры

Обратите внимание, как этот фрагмент вывода поможет понять:

  • какие глобальные объекты вносят наибольший вклад;
  • какие таблицы и строки занимают много места.

Связка с выводом go tool compile -m

Команда:

go build -gcflags="-m" main.go

покажет вам:

  • что было аллоцировано на куче;
  • какие переменные промотированы;
  • что осталось в статике.

Хотя она не показывает напрямую constants-segment, вы поймете, какие данные оказались на куче, а какие — нет. Всё, что не на куче и не на стеке, а при этом глобально доступно и константно — почти всегда в сегменте статических/константных данных.

Типичные паттерны использования сегмента констант

Паттерн: таблица переходов или lookup table

Давайте посмотрим, что происходит в следующем примере:

package lexer

// Таблица классов символов для лексера
var charClass = [256]byte{
    /* 0..31 */ 0, 0, 0, 0, /* ... заполнение ... */ 0,
    // ...
}

// Функция, использующая таблицу
func Class(ch byte) byte {
    // Обращаемся к глобальному массиву
    return charClass[ch]
}

Особенности:

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

Паттерн: таблица хэшей или CRC

package crc32

// Пример из стандартной библиотеки (упрощенно)
var table = [256]uint32{
    0x00000000, 0x77073096, /* ... много значений ... */, 0x2D02EF8D,
}

func update(crc uint32, p []byte) uint32 {
    for _, b := range p {
        crc = table[byte(crc)^b] ^ (crc >> 8)
    }
    return crc
}

Здесь:

  • огромная таблица значений CRC зашита в бинарник;
  • это классический пример использования сегмента констант для ускорения вычислений;
  • цена — увеличенный размер .rodata.

Паттерн: embed и константы

Начиная с Go 1.16, можно встраивать файлы с помощью директивы //go:embed. Такие данные тоже попадают в бинарник и во многом ведут себя как большие константные массивы байт.

Пример:

package main

import _ "embed"

//go:embed config/default.yaml
var defaultConfig []byte

func main() {
    // Массив байт лежит в бинарнике и в сегменте данных/констант
    println(len(defaultConfig))
}

Важный момент:

  • большие файлы, встроенные через embed, могут серьезно раздуть размер бинарника;
  • эти данные по сути хранятся в аналогичной константной области, хоть и представлены как слайс байт.

Заключение

Сегмент constants (constants-segment) в Go — это ключевая часть механизма работы с неизменяемыми данными:

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

Для вас, как разработчика, это означает:

  • вы можете смело использовать строковые и числовые константы, не опасаясь лишних аллокаций;
  • большие таблицы и встроенные данные нужно оценивать с точки зрения размера бинарника и потребления памяти;
  • понимание того, какие данные попадают в сегмент констант, помогает анализировать производительность и поведение GC.

Если вам нужно еще глубже, можно изучить исходники рантайма Go (директория src/runtime), посмотреть, как описаны границы rodata и других областей, и сопоставить это с результатами go tool objdump и objdump системного уровня.

Частозадаваемые технические вопросы по теме и ответы

Как понять, какие именно глобальные объекты попали в константный сегмент, а какие в обычный data

На уровне Go-инструментов вы видите только косвенно. Практический путь:

  1. Соберите бинарник с go build -ldflags="-s -w" -gcflags="-c=2".
  2. Используйте nm -S app или go tool nm app для списка символов с размерами.
  3. Сопоставьте имена переменных с секциями через objdump -h app и objdump -t app.
  4. Символы в секции .rodata и родственных — это ваш константный сегмент.

Можно ли заставить компилятор не класть большие таблицы в сегмент констант

Напрямую — нет, но можно:

  1. Инициализировать таблицу в init руками, а не литералом:
    • тогда данные окажутся на куче или в bss + код инициализации.
  2. Хранить данные в сжатом виде (строка/байтовый слайс) и распаковывать по требованию.
  3. Вынести данные во внешний файл и читать в рантайме.

Это осознанный компромисс между временем старта, размером бинарника и потреблением памяти.

Как повлиять на то, распознает ли компилятор область как noptr (без указателей)

На уровне Go-кода это делается косвенно:

  1. Используйте типы без указателей (например, int, float64, struct без полей-указателей).
  2. Избегайте в таких структурах полей типа interface{}, string, []T — они содержат указатели.
  3. Компилятор сам разнесет такие объекты по noptrdata / noptrbss и обычным секциям.

Ручного указания "эта переменная точно без указателей" в Go нет, все определяется по типу.

Влияет ли использование const вместо var на размещение данных в constants-segment

Косвенно:

  • const часто вообще не требует памяти — значения встраиваются в инструкции;
  • var, инициализируемая константным литералом, создает реальный объект в памяти (глобальный или локальный), который может ссылаться на сегмент констант (строки, массивы).

Если цель — минимизировать статические данные, используйте const, когда возможно.

Можно ли считать, что все данные в constants-segment никогда не сканируются GC

Не совсем:

  • часть областей в rodata действительно может не сканироваться (если гарантированно без указателей);
  • данные с указателями (метаданные типов, таблицы интерфейсов) описываются специальными структурами в рантайме, и GC знает, где ожидать указатели.

В любом случае сами константные области не освобождаются и не перемещаются, GC только учитывает хранящиеся там ссылки на объекты в куче.

Стрелочка влевоСегмент services - как проектировать services-segment в микросервисной архитектуреСегмент components - как использовать components-segment в интерфейсных проектахСтрелочка вправо

Все гайды по Feature-sliced_design

Открыть базу знаний

Отправить комментарий