Cgo: цена вызова C из Go и когда без него не обойтись
Вызов C-функции из Go стоит десятки-сотни наносекунд накладных расходов на каждый переход границы. Разбираем, откуда берётся эта цена, как её измерить и в каких случаях cgo действительно нужен.

Микробенчмарк на Go 1.22: пустой вызов Go-функции — единицы наносекунд, пустой вызов C-функции через cgo — десятки наносекунд, а в некоторых конфигурациях граница переносится в диапазон сотен наносекунд, если рантайму приходится переключать состояние горутины. На горячем пути в тысячи вызовов в секунду эта разница становится заметной статьёй в профиле. Разберём, почему переход Go→C дороже обычного вызова, что именно происходит на границе, как это мерить и когда cgo всё равно остаётся единственным вариантом.
Что происходит на границе Go→C
Go-рантайм управляет горутинами, стеком и планировщиком. Стек горутины растёт и перемещается в памяти по мере необходимости. C-код ничего про это не знает: он ожидает большой непрерывный стек фиксированного расположения и не терпит, чтобы у него из-под ног переезжала память.
Поэтому при вызове C-функции рантайм не может просто прыгнуть по адресу. Он выполняет служебную работу вокруг вызова:
- переключается со стека горутины на отдельный системный стек (g0), связанный с потоком ОС;
- помечает, что горутина ушла в C и её стек трогать нельзя, а сам поток временно выпадает из-под управления планировщика — как при блокирующем системном вызове;
- после возврата из C восстанавливает состояние и возвращает поток планировщику.
Именно эта обвязка и есть цена вызова. Она не зависит от того, что делает C-функция: даже пустая функция, возвращающая ноль, платит за переход границы.
Cgo не бесплатен и никогда таким не был. Правило простое: пересекайте границу редко и с большими порциями работы, а не часто и по мелочи.
Почему поток выпадает из планировщика
Пока горутина внутри C, рантайм не может её вытеснить: у него нет контроля над C-стеком. Если C-функция выполняется дольше отведённого кванта, планировщик считает поток занятым и при нехватке параллелизма может завести новый поток ОС, чтобы обслуживать остальные горутины. Отсюда второй, менее очевидный эффект cgo: долгие C-вызовы способны раздувать число потоков ОС, а не только тормозить конкретный вызов.
Как измерить цену на своём железе
Абсолютные наносекунды зависят от процессора, версии Go и того, что делает функция. Не переносите чужие цифры в свой проект — померьте сами. Ниже минимальный бенчмарк, который сравнивает пустой Go-вызов и пустой cgo-выз」 (Go 1.22, файл bench_test.go в отдельном пакете).
package cgobench
/*
// пустая C-функция: платим только за переход границы
static int noop(void) { return 0; }
*/
import "C"
// noopGo — эталон: обычный вызов Go без пересечения границы
func noopGo() int { return 0 }
//go:noinline
func noopGoNoinline() int { return 0 }
package cgobench
import "testing"
func BenchmarkGoCall(b *testing.B) {
var sum int
for i := 0; i < b.N; i++ {
sum += noopGoNoinline() // без noinline компилятор всё выкинет
}
_ = sum
}
func BenchmarkCgoCall(b *testing.B) {
var sum int
for i := 0; i < b.N; i++ {
sum += int(C.noop())
}
_ = sum
}
Запуск:
go test -bench=. -benchmem -count=5 ./cgobench/
Прогоняйте -count=5 и сравнивайте разброс, а не одно число. Если хотите статистически честное сравнение — сохраните вывод в файл и прогоните через benchstat (устанавливается командой go install golang.org/x/perf/cmd/benchstat@latest). Смотрите на разницу между двумя бенчмарками в наносекундах на операцию — это и есть цена перехода границы на вашем железе.
Что искажает замер
- Инлайнинг. Пустую Go-функцию компилятор встроит и выкинет, и вы сравните cgo с нулём. Директива
//go:noinlineэто блокирует. - Мёртвый код. Если результат никуда не идёт, вызов удалят целиком. Аккумулируйте результат в переменную и присвойте её blackhole-переменной пакета или используйте, как в примере выше.
- Тепловое состояние CPU. На ноутбуке частота плавает; для честных цифр фиксируйте частоту или мерьте на сервере.
Где цена вырастает сверх базовой
Базовая цена перехода — это ещё не всё. Есть операции, которые добавляют сверху:
- Передача Go-указателей в C. Действуют правила cgo pointer passing: Go-память, на которую ссылается переданный указатель, не должна сама содержать Go-указателей, а C не имеет права хранить этот указатель после возврата. Проверки этих правил включаются переменной окружения
GODEBUG=cgocheck=...и добавляют накладные расходы во время выполнения. - Строки и слайсы.
C.CStringвыделяет память в C-куче и копирует данные; вы обязаны сами вызватьC.free. Обратное преобразованиеC.GoStringтоже копирует. Каждое копирование — это аллокация и работа, которой в чистом Go нет. - Колбэки C→Go. Обратный вызов Go-функции из C ещё дороже перехода Go→C и требует экспорта функции через
//export. По возможности их избегают.
Одна крупная порция вместо тысячи мелких
Если вам нужно обработать миллион элементов C-библиотекой, не вызывайте C-функцию в цикле на каждый элемент. Соберите данные в один буфер, передайте его целиком одним вызовом и разберите результат. Так вы платите за границу один раз, а не миллион.
Когда без cgo действительно нельзя
Не каждая интеграция требует cgo. Разложим варианты по цене и применимости.
| Ситуация | Нужен ли cgo | Альтернатива |
|---|---|---|
| Зрелая C/C++ библиотека без аналога на Go (кодеки, криптопримитивы вендора, проприетарные SDK) | Да, чаще всего | Иногда есть чистый Go-порт — проверьте перед привязкой |
| Системный вызов, которого нет в стандартной библиотеке | Не всегда | Прямой syscall через golang.org/x/sys/unix |
| Библиотека предоставляет C-ABI и стабильна | Можно без cgo | Динамическая загрузка через plugin или FFI-мост, либо вынести в отдельный процесс и общаться по gRPC/pipe |
| Нужна максимальная переносимость и статическая сборка | Лучше без cgo | Чистый Go: CGO_ENABLED=0 даёт полностью статический бинарник |
| Разовая тяжёлая обработка (перекодирование, ML-инференс через C-рантайм) | Да | Или отдельный сервис на нужном языке |
Ключевой критерий: cgo оправдан, когда за один переход границы выполняется много полезной работы. Тяжёлый декодер видео, который принимает пакет и возвращает кадр, — хороший кандидат. Функция, которую дёргают на каждый элемент горячего цикла, — плохой.
Что теряете вместе с cgo
Цена вызова — не единственная плата. Включив cgo, вы соглашаетесь на пакет ограничений:
- Кросс-компиляция усложняется. Простой
GOOS=linux GOARCH=arm64 go buildдля cgo-проекта требует C-тулчейна под целевую платформу, а не только Go. - Сборка замедляется — подключается компилятор C.
- Статический бинарник получить труднее: по умолчанию появляется динамическая линковка с системными библиотеками (например libc).
- Профилирование и отладка сложнее: pprof видит Go-стек, но не заглядывает внутрь C так же удобно.
- Ответственность за память C целиком на вас: сборщик мусора Go её не трогает, утечки и use-after-free возвращаются во всей красе.
Что делать дальше
Прежде чем тянуть cgo в проект: проверьте, нет ли чистого Go-порта нужной библиотеки — для многих популярных C-библиотек он существует. Если cgo неизбежен, замерьте цену перехода на своём железе бенчмарком выше, спроектируйте API так, чтобы граница пересекалась крупными порциями, и держите CGO_ENABLED=0 как значение по умолчанию для всех сборок, где C не нужен. В проде следите за числом потоков ОС процесса (например через runtime.NumGoroutine в связке с метриками планировщика и системным счётчиком потоков): рост числа потоков при неизменной нагрузке — частый признак долгих блокирующих C-вызовов. Точные правила передачи указателей и текущее поведение cgocheck сверяйте с официальной документацией пакета cgo для вашей версии Go.
Создатель презентаций: Структура и текст каждого слайда: заголовки-утверждения, одна мысль на слайд, заметки докладчику.
Спросить за 150 ₽Источники: Command cgo — официальная документация Go, cgo is not Go — блог Дэйва Чейни
Частые вопросы
А если у меня Go другой версии — цифры бенчмарка будут другими?
Почему мой Go-бенчмарк показывает 0 нс на вызов?
Cgo раздувает число потоков ОС — это точно из-за него?
Можно ли собрать статический бинарник с cgo?
Я передаю Go-слайс в C, и иногда всё падает. Почему?
Всегда ли системный вызов требует cgo?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.