Перейти к содержимому

Go Compiler, CGO и Assembly: глубокая часть

Здесь мы разбираем, что компилятор Go делает с твоим кодом: inlining, escape analysis, BCE, devirtualization, PGO. Затем — build constraints, conditional compilation. Дальше — CGO с его подводными камнями и pure-Go альтернативами. Наконец — основы Plan 9 assembly и go:linkname, как linker-trick для приватных символов. На Middle 2 эти вопросы выходят на собесах в Авито/Яндекс/Тинькофф, когда уже разобрали runtime. Это уровень “знаю, как мой код превращается в машинный язык, понимаю, почему какие-то вещи нельзя делать в CGO, могу читать ассемблер runtime’а”.

  1. Базовая концепция (для разогрева)
  2. Glubokoe погружение: compiler passes, build tags, CGO, assembly, linkname
  3. Подводные камни
  4. Производительность и реальные кейсы
  5. Вопросы на собесе Middle 2
  6. Practice
  7. Источники

Go-компилятор (gc, не путать с garbage collector):

  1. Лексер + парсер → AST.
  2. Type checker.
  3. SSA (static single assignment) intermediate representation.
  4. Multiple optimization passes (inlining, escape, BCE, devirtualization, PGO).
  5. Machine code generation (asm-уровень).
  6. Linker (link) — финальный binary.

Build tags позволяют условную компиляцию: //go:build linux, //go:build amd64 && !race. Файлы с суффиксами _linux.go, _amd64.go автоматически включаются.

CGO — мост к C-библиотекам. Полезно для SQLite, OpenSSL, libpq. Дорого: переход G→C блокирует M, нет автоматического GC поверх C-памяти.

Assembly в Go — Plan 9-style, отличается от Intel/AT&T. Файлы *_amd64.s. Использовать редко, но в runtime критично (атомики, SIMD, syscall).

go:linkname — directive для linker’а, позволяет ссылаться на private символы других пакетов. С Go 1.23+ ограничения сильно ужесточены.


*.go files
[parser] ─► AST (abstract syntax tree)
[typecheck] ─► типизированный AST
[walk] ─► упрощённый AST (replace high-level operators, e.g. map ops → runtime calls)
[SSA conversion] ─► SSA form (static single assignment)
[SSA optimization passes]
├─ dead code elimination
├─ common subexpression elimination
├─ value range analysis
├─ bounds check elimination
├─ inlining (расширение функций inline)
├─ escape analysis (heap vs stack)
├─ devirtualization (interface call → static call)
├─ PGO inlining adjustment (Go 1.21+)
[machine code generation] ─► *.o object files
[linker] ─► binary

Просмотр результатов:

Окно терминала
go build -gcflags="-m" # inlining / escape
go build -gcflags="-m=2" # подробнее
go build -gcflags="-S" # ассемблер
go tool compile -W # SSA passes
GOSSAFUNC=foo go build . # HTML SSA для функции foo

Цель: заменить function call на тело функции в месте вызова. Экономит function call overhead (~5 ns) и открывает дополнительные оптимизации (constant folding в caller’е).

Решение принимается на основе budget:

  • Каждая функция имеет “цену” в условных единицах.
  • Простые операторы (+, -, *, /) = 1.
  • Function calls внутри = ~57 + аргументы.
  • Default budget = 80.

Если функция стоит ≤ 80 — inlined automatically. Можно подсветить:

Окно терминала
go build -gcflags="-m -m" main.go 2>&1 | grep "can inline"

⚠️ Что запрещает inlining:

  • Recursive calls.
  • select, switch, for range (некоторые случаи).
  • Closures (раньше — всегда, сейчас иногда инлайнятся).
  • Calls с unsafe.Pointer.
  • Method calls через interface (require devirtualization first).

//go:noinline директива — запрещает inlining конкретной функции (для тестов, бенчмарков).

Каждый array/slice access s[i] имеет неявный bounds check:

// s[i]:
if uint(i) >= uint(len(s)) { runtime.panicIndex(i, len(s)) }
return s[i]

Этот check стоит ~1-2 ns. Компилятор стремится удалить его, когда может доказать, что i в пределах:

for i := 0; i < len(s); i++ {
_ = s[i] // BCE: компилятор знает i < len(s)
}
n := len(s)
for i := 0; i < n; i++ {
_ = s[i] // НЕ BCE: n не связан с len(s) для компилятора
}

⚠️ Capture len(s) в переменную может убить BCE (особенно если s потом меняется).

Проверка:

Окно терминала
go build -gcflags="-d=ssa/check_bce/debug=1" main.go

Покажет, где остались bounds checks.

Trick для оптимизации:

// до:
for i := 0; i < len(s); i++ {
s[i] = s[i+1] // i+1 — компилятор не уверен, BC остаётся
}
// после:
_ = s[len(s)-1] // hint, что весь диапазон валиден
for i := 0; i < len(s)-1; i++ {
s[i] = s[i+1] // BCE сработает
}

Interface call = indirect call через itab (interface table). Это ~5 ns + cache misses. Если компилятор может доказать конкретный тип — заменяет на static call.

var w io.Writer = os.Stdout
w.Write(p) // в Go 1.19+ может быть devirtualized: os.Stdout.Write(p)

Условия:

  • Compiler видит конкретный тип в той же compilation unit.
  • Type не меняется между assign и use.

⚠️ Devirtualization умеет только static types. Если ты:

w := func() io.Writer {
if cond { return os.Stdout } else { return os.Stderr }
}()
w.Write(p) // нет devirt

— невозможно дегвиртуализировать.

Компилятор находит код, который никогда не выполнится:

if false {
expensive() // удалено
}
const debug = false
if debug {
fmt.Println("debug") // удалено
}

Это используется для conditional compilation через константы (вместо build tags для простых случаев).

Решает: переменная остаётся на стеке (free) или escape’ит в heap (требует allocation + GC).

Эвристики:

  • Адрес переменной возвращается из функции → escape.
  • Адрес сохранён в global / поле escaped struct → escape.
  • Передан в interface → escape (потому что interface boxing требует heap).
  • Передан в goroutine (go func() { use(&v) }()) → escape.
  • Sent в channel → escape (если value содержит pointer).
  • Слайс растёт через append, оригинал был на стеке → escape.
Окно терминала
go build -gcflags="-m" main.go

Покажет:

./main.go:5: moved to heap: x
./main.go:8: &x escapes to heap

⚠️ Escape — не плохо само по себе. Но в hot path избыточный escape = больше GC pressure. Тщательная типизация (concrete types вместо interfaces) часто уменьшает escape.

Идея: компилятор использует runtime profile (CPU pprof) для оптимизации. Знает горячие функции, горячие call sites — увеличивает их inlining budget.

Поток:

  1. Запустить prod-сервис с pprof CPU profile.
  2. Скачать profile (go tool pprof -proto > default.pgo).
  3. Положить в корень модуля под именем default.pgo.
  4. Build: go build . (PGO применится автоматически).

Результат: 2-7% улучшение throughput на реальных сервисах (Go-team replicate в pkg.go.dev, Uber, Cloudflare).

⚠️ Profile должен быть репрезентативным (production-like нагрузка). Bad profile может ухудшить performance.

Окно терминала
GOSSAFUNC=main.foo go build .
# создаёт ssa.html с интерактивным просмотром каждой SSA pass

Открыть в браузере — посмотришь, как функция трансформируется через 30+ passes. Очень полезно для оптимизаций.


//go:build linux && amd64
package mypkg

Старый syntax (// +build) тоже работает, но deprecated с 1.17. Современные проекты — //go:build.

//go:build linux // только Linux
//go:build linux || darwin // Linux или macOS
//go:build linux && (amd64 || arm64) // Linux на 64-bit
//go:build !race // если НЕ race
//go:build go1.22 // Go 1.22+
//go:build mytag // пользовательский tag
foo_linux.go // только linux
foo_amd64.go // только amd64
foo_linux_amd64.go // только linux + amd64
foo_test.go // только при `go test`
Окно терминала
go build -tags="dev" # включает //go:build dev
go build -tags="dev,verbose" # несколько
go build -tags="prod !dev" # prod и без dev
debug.go
//go:build debug
package mylib
const enableDebug = true
// release.go
//go:build !debug
package mylib
const enableDebug = false

Использование:

if enableDebug { // компилятор уберёт, если const false
log.Print(...)
}
fs_linux.go
//go:build linux
package fs
func FAdvise(fd int) error { return syscall.Fadvise(...) }
// fs_other.go
//go:build !linux
package fs
func FAdvise(fd int) error { return nil } // no-op

CGO позволяет вызывать C-код из Go. Простейший пример:

package main
/*
#include <stdio.h>
void hello(const char* name) {
printf("Hello, %s!\n", name);
}
*/
import "C"
func main() {
C.hello(C.CString("World"))
}

CGO активно используется для:

  • SQLite (github.com/mattn/go-sqlite3).
  • OpenSSL.
  • libpq (PostgreSQL — github.com/lib/pq использует CGO).
  • Системные библиотеки (libpcap, libfuse).
  • Машинное обучение (TensorFlow, ONNX runtime).

Когда Go-горутина вызывает C-функцию:

  1. Runtime отвязывает горутину от P (как при syscall, через cgocall).
  2. Стек переключается с Go-стека на C-стек (отдельный, ~8 MB по-умолчанию).
  3. M исполняет C-код. P может быть переотдан другой M.
  4. C возвращает → Runtime пытается схватить P обратно.
  5. Goroutine продолжается на Go-стеке.

⚠️ Это дорого: ~50-200 ns на CGO call просто overhead. Плюс — Go scheduler не контролирует M, пока он в C.

Go pointer нельзя хранить в C-памяти. GC не видит C-памяти, объект может быть собран:

goVar := myStruct{}
C.set_callback(&goVar) // ⚠️ ОПАСНО: C хранит &goVar, GC может собрать

Правильно — через cgo.Handle:

h := cgo.NewHandle(&goVar)
C.set_callback(unsafe.Pointer(h))
// ...
// в callback:
// func goCallback(h C.uintptr_t) {
// ref := cgo.Handle(h).Value().(*myStruct)
// ...
// }
h.Delete() // освобождаем, когда не нужно

cgo.Handle (Go 1.17+) держит ссылку на Go-объект в runtime’е, GC не уберёт пока handle жив.

ptr := C.malloc(1024)
defer C.free(ptr)

GC не отслеживает эту память. Утечки = твоя ответственность.

Если C-функция выполняется > 20ms, sysmon переотдаст P. Если есть много long C-calls — может вырасти thread count.

Окно терминала
# Видно через
GODEBUG=schedtrace=1000 ./mybinary
# threads растёт быстро
Окно терминала
CGO_ENABLED=0 go build .

Это режим pure-Go. Если код использует import "C" — build fail. Полезно для:

  • Static binaries в Docker FROM scratch.
  • Cross-compilation без C-toolchain.

Для кросс-компиляции с CGO нужен C-toolchain для целевой архитектуры:

Окно терминала
# для linux/arm64 на macOS
CGO_ENABLED=1 GOOS=linux GOARCH=arm64 CC=aarch64-linux-gnu-gcc go build .

Это ад. Решение: pure-Go альтернативы.

Окно терминала
CGO_ENABLED=1 go build -ldflags '-extldflags "-static"' .

Зависит от того, как линкуется libc (glibc vs musl). На Alpine (musl) проще, на Debian (glibc) — могут быть проблемы.

C-libraryCGO versionPure-Go alternative
libpqgithub.com/lib/pqgithub.com/jackc/pgx (рекомендуется)
sqlite3github.com/mattn/go-sqlite3modernc.org/sqlite
OpenSSLgithub.com/spacemonkeygo/opensslcrypto/tls (stdlib)
zlibgithub.com/mongodb/zstd (CGO)compress/gzip (stdlib)
libcurlчерез CGOnet/http

Pure-Go обычно медленнее (1.5-2x для крипты), но даёт:

  • Static binaries.
  • Простую cross-compilation.
  • Нет GC issues с C-памятью.
  • Меньше attack surface (нет C-vulnerabilities).

В большинстве production кейсов pure-Go — правильный выбор.

Нужно тогда, когда C-библиотека требует callback:

//export goCallback
func goCallback(value C.int) {
fmt.Println("got:", value)
}
// в C-части:
// extern void goCallback(int);
// void doWork() {
// for (int i = 0; i < 10; i++) {
// goCallback(i);
// }
// }

При callback C→Go:

  1. CGO runtime знает, что мы в C-режиме.
  2. Переключается на Go-стек.
  3. Захватывает P (может ждать!).
  4. Вызывает Go-функцию.
  5. Возвращается в C.

⚠️ Если callback вызывается часто из tight C-loop — overhead огромный. Лучше batch’и: C собирает результаты, один callback возвращает массив.


Go использует Plan 9-style assembly, не Intel и не AT&T. Это собственный стиль с уникальными directives.

foo_amd64.s
#include "textflag.h"
TEXT ·myFunc(SB), NOSPLIT, $0-16
MOVQ a+0(FP), AX // загрузка аргумента a (offset 0)
MOVQ b+8(FP), BX // загрузка аргумента b (offset 8)
ADDQ BX, AX
MOVQ AX, ret+0(FP) // ⚠ ret offset depends — see below
RET
  • TEXT ·myFunc(SB), flags, $framesize-argsize — объявление функции. · — package qualifier. SB — static base (ссылка на символ).
  • NOSPLIT — функция не имеет stack growth check (для маленьких функций).
  • NOFRAME — нет frame pointer.
  • WRAPPER — wrapper function (для defer).
  • На amd64: AX, BX, CX, DX, BP, SP, DI, SI, R8-R15.
  • На arm64: R0-R30, RSP.
  • FP (frame pointer) — pseudo-register для доступа к аргументам и return values. a+0(FP) = первый аргумент по смещению 0.
  • SB (static base) — pseudo для глобальных символов.
  • SP — текущий stack pointer (но в Plan 9 это псевдо! Реальный SP — SP без префикса).

До Go 1.17 — все аргументы и returns через стек (с FP). С 1.17 — ABIInternal (регистровая), но Go обычно сам делает wrapper’ы (ABI0 ↔ ABIInternal).

В assembly пишем обычно через FP, что соответствует ABI0:

TEXT ·Add(SB), NOSPLIT, $0-24
MOVQ a+0(FP), AX
MOVQ b+8(FP), BX
ADDQ BX, AX
MOVQ AX, ret+16(FP)
RET

Для assembly-функций без Go-тела (часто SIMD). Говорит компилятору: эта функция не делает escape своих аргументов, можно оставить на стеке.

//go:noescape
//go:linkname add internal/asmpkg.add
func add(a, b uint64) uint64
// реализация в add_amd64.s

Запрещает stack growth check в прологе. Для очень маленьких functions, где fontainer-check был бы overhead. Используется в runtime’е активно.

⚠️ Если nosplit-функция действительно расширяет стек — ВЕРОЯТНО stack overflow без обнаружения. Использовать осторожно.

Это очень мощная directive. Позволяет ссылаться на private символы других пакетов (бы что-то приватное):

package mypkg
import _ "unsafe" // нужен для linkname
//go:linkname runtimeNow runtime.now
func runtimeNow() (sec int64, nsec int32, mono int64)
func MyFunc() {
s, n, _ := runtimeNow()
...
}

Это позволяет вызывать runtime.now — приватную функцию runtime’а — извне!

Используется в stdlib:

  • sync/atomicruntime (для memory ordering).
  • timeruntime (для monotonic clock).
  • os/execos (для FD inheritance).

⚠️ С Go 1.23+ strict checking: linkname на runtime символы из user-кода работает только если “approved” (есть в списке в cmd/compile). Это сделано, потому что многие пакеты использовали linkname как “хак”, и при изменении runtime ломались. Сейчас Go team говорит: “это запрещено вне stdlib”.

С Go 1.25 — ещё строже. Cильное запрещение, может пропадает в Go 2.x.

Некоторые функции компилятор знает специально — они не вызываются как обычные, а инлайнятся напрямую в machine code:

  • runtime.memmove → CALL без обычного call overhead.
  • Атомики (atomic.AddInt64, atomic.LoadPointer) → CMPXCHG / LOCK XADD напрямую.
  • Hash functions (runtime.memhash) → специальный код, основан на AES-NI / SSE.
  • Some math (math.Sqrt, math.Abs) → одна SSE-инструкция.
  • unsafe.Sizeof, unsafe.Alignof — compile-time constants.

Это даёт zero-overhead для критичных операций.

Стандартная либа использует SIMD для крипты:

  • chacha20crypto/internal/chacha20.
  • sha256 — SHA-NI и AVX2.
  • aes — AES-NI.

Пример (упрощённо, chacha20):

TEXT ·xorKeyStreamVX(SB), NOSPLIT, $0
...
VPXOR Y0, Y4, Y0 // 256-bit XOR
VMOVDQU Y0, 0(DI) // store 32 bytes
...

Reading чужого assembly в Go — это src/runtime/asm_amd64.s, src/crypto/*/asm_amd64.s.

Большинство Middle 2 разработчиков не пишут assembly. Достаточно:

  • Понимать, что runtime-функции (atomics, hash) — это assembly intrinsics.
  • Уметь читать stack trace с asm-уровнем (например, при panic в runtime.memmove).
  • Знать про //go:noescape и //go:linkname (что они есть, когда видишь).

Но если занимаешься performance-критичным кодом — основы assembly помогают понимать generated code от компилятора.


func Big() {
// ~200 line function
}
func Small() { Big() }

Big() не inlinable (превышает budget). Small() тоже не выиграет от inlining Big’а.

Если ты ожидал inlining — проверь через -m=2.

Без него компилятор может заинлайнить замеряемую функцию, и бенчмарк покажет ноль. Стандартный паттерн:

//go:noinline
func target() { ... }
func BenchmarkTarget(b *testing.B) {
for i := 0; i < b.N; i++ {
target()
}
}

3.3. ⚠️ Bounds check возвращается из-за переменной

Заголовок раздела «3.3. ⚠️ Bounds check возвращается из-за переменной»
s := make([]int, 100)
for i := 0; i < 100; i++ {
s[i] = i // BCE: i < 100 < len(s) ⇒ BC eliminated
}
for i := 0; i < count; i++ { // count из аргументов
s[i] = i // BC сохранится (count может быть > len(s))
}
var x int = 42
fmt.Println(x) // x escape'ит, потому что fmt.Println(args ...any)

Любая передача в any — обычно escape. В hot path избегать (использовать typed callbacks).

Если profile собран с нерелевантной нагрузкой (например, dev environment вместо prod) — PGO может ухудшить performance. Проверяй diff бенчмарков с и без PGO.

//go:build linux,amd64 // ⚠️ ОШИБКА (нужно &&)
//go:build linux && amd64 // правильно

Старый +build использовал запятые. Новый //go:build — логические операторы.

FROM golang:1.22 AS build
COPY . .
RUN go build . # ⚠️ если используется CGO, нужен gcc
FROM scratch
COPY --from=build /app /app
ENTRYPOINT ["/app"]

scratch не имеет libc. Если CGO=1 → dynamic linking к libc → fail.

Решение: CGO_ENABLED=0 (если возможно) или multi-stage с Alpine (musl libc, static).

C-loop, который вызывает Go callback миллион раз → миллион переключений контекстов → 200 ns × 1M = 200 ms overhead.

Решение: batch (массив результатов в C, один callback с массивом).

unsafe.Pointer — это абстрактный pointer для Go. C.uintptr_t — C-pointer. Конверсии: unsafe.Pointer(uintptr(p)). Не путать.

// Intel:
mov rax, [rbx+8]
// Plan 9:
MOVQ 8(BX), AX

Обратный порядок операндов. Регистры без префикса r/e. Q = quadword (64-bit). L = longword (32-bit).

Если ты используешь linkname к private runtime symbol — следующее обновление Go может убрать/переименовать символ, твой код упадёт. Кстати, с 1.23+ это в принципе запрещено вне stdlib.

Cross-compile pure-Go = GOOS=linux GOARCH=arm64 go build . — работает. Cross-compile с CGO = нужен C cross-compiler. Часто проще использовать Docker:

FROM --platform=$BUILDPLATFORM golang:1.22 AS build
ARG TARGETOS TARGETARCH
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build .
TEXT ·foo(SB), NOSPLIT, $1024-0
// 1024-байт frame, без stack growth check!

Если стек горутины ~2 KB начальный, и foo резервирует 1024 — может быть всего 1 KB запаса. Следующая функция упирается в guard — fatal “morestack on g0” или подобное.

NOSPLIT только для функций с frame < 96 байт (примерно).

Иногда inlined function escape-ит свои аргументы, что увеличивает escape в caller’е. Bench показывает, что non-inlined быстрее. Очень редко, но бывает.

Если C-библиотека делает callback в Go, и callback ожидает thread-local state (e.g., GLX context для OpenGL) — обязательно runtime.LockOSThread() в main.

func main() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
// GLX setup
C.startMainLoop() // C вызывает Go callbacks на этом же thread
}

Кейс (e-commerce checkout): после внедрения PGO с production profile, throughput вырос на 5.2%, P99 latency упала на 8%. Стоимость — генерация profile + extra build step.

Кейс (CLI tool): использовали mattn/go-sqlite3 (CGO). Cross-compile для Windows/Mac/Linux — три CI matrix. Static binary не получался.

Перешли на modernc.org/sqlite. Скорость упала на ~30% (sqlite queries), но:

  • CGO_ENABLED=0 везде.
  • Один Dockerfile.
  • Простой cross-compile.
  • Binary 2 MB вместо 8 MB.

Кейс (TLS-handling): на серверах M1 (ARM64) Go сам выбирал ARM64-asm для chacha20. Throughput 3 Gbit/sec. На amd64 с AVX2 — 5 Gbit/sec. Без SIMD — 800 Mbit/sec.

Кейс (микросервис с custom time): нужны были monotonic timestamps в naseconds resolution. time.Now() возвращает Time с monotonic clock — но извлечь uint64 ns нет публичного API.

Через linkname к runtime.nanotime — extract напрямую. Overhead time.Now ~30 ns, nanotime ~5 ns. На 1M ops/sec — выигрыш 25 ms/sec CPU.

⚠️ С Go 1.23 это деприкейтнули. Сейчас разрешено только в самой стандартной либе.

Кейс (мобильное SDK на Go): разные builds для feature gates.

//go:build !nosocial
package myapp
// social features

Build без social:

Окно терминала
go build -tags="nosocial" .

Binary меньше на 2 MB, нет dependency-зависимостей social-фич.

Кейс (low-latency trading): hot function 200 ns/op. После переписывания с использованием //go:noinline для НЕ-critical helper functions + ensure inlining for critical ones (через manual splitting) — 130 ns/op.

Контроль через -gcflags="-m=2" обязателен.

Кейс (Go 1.19 release): сервис с тысячами io.Writer calls. Compiler в 1.19 научился devirtualize, throughput вырос на 4-7%.


Q1. Опишите стадии Go compiler.

Parser → typecheck → walk → SSA conversion → SSA optimization passes (inlining, escape, BCE, DCE, devirt) → machine code → linker.

Q2. Что такое inlining и какой budget?

Подстановка тела функции в caller. Budget 80 условных единиц (простой operator = 1, function call = 57). Превышение — нет inlining. Управление через //go:noinline.

Q3. Что такое BCE и как проверить?

Bounds check elimination — удаление неявных if i >= len(s) { panic } если компилятор уверен, что safe. Проверка: go build -gcflags="-d=ssa/check_bce/debug=1".

Q4. Что такое devirtualization и когда появилось?

Замена interface call (через itab) на static call. Появилось в Go 1.19. Условие: compiler видит конкретный тип в той же compilation unit.

Q5. Что такое escape analysis?

Решение compile-time: переменная на стеке или в heap. Escape: возврат адреса, передача в interface, в goroutine, в channel. Без escape — free allocation на стеке.

Q6. Как посмотреть escape analysis?

go build -gcflags="-m" main.go — базовый вывод. -m=2 — детальный.

Q7. Что такое PGO и сколько даёт?

Profile-guided optimization (Go 1.21+). Compiler использует CPU profile для оптимизации (inlining hot functions агрессивнее). На прод-сервисах +2-7% throughput.

Q8. Какой синтаксис build tags современный?

//go:build linux && amd64. Логические &&, ||, !. Старый // +build deprecated с 1.17.

Q9. Что такое суффиксы _linux.go, _amd64.go?

Файлы с этими суффиксами автоматически имеют implicit build constraint. _linux.go = //go:build linux, без надобности писать явно.

Q10. Зачем CGO?

Вызов C-библиотек: SQLite, OpenSSL, libpq, GUI-frameworks. Pure-Go альтернативы часто медленнее, но проще в deploy.

Q11. Какие подводные камни CGO?

  • Дорогой переход G→C (50-200 ns).
  • Go pointers нельзя хранить в C-памяти (GC не видит).
  • Long C-calls блокируют M, sysmon выделяет новые threads.
  • Cross-compile сложно (нужен C cross-compiler).
  • Static link тоже сложно (libc dependencies).

Q12. Что такое cgo.Handle?

Способ передать Go-объект в C-код без риска GC. cgo.NewHandle(obj) возвращает opaque uintptr, который C хранит. Handle.Value() возвращает оригинальный объект. Runtime держит ref пока handle жив.

Q13. Почему CGO_ENABLED=0 нужен для Docker scratch?

scratch не имеет libc. С CGO компилятор линкует к libc (dynamic). Static linking с CGO работает с musl (Alpine), но сложен с glibc.

Q14. Альтернативы CGO для PostgreSQL?

github.com/jackc/pgx — pure-Go, рекомендуется. Используется в production Tinkoff/Avito.

Q15. Альтернативы CGO для SQLite?

modernc.org/sqlite — pure-Go (автогенерирован из C через c2go). Медленнее CGO версии на ~30%, но static binary, простой cross-compile.

Q16. Какой assembly использует Go?

Plan 9 syntax. Отличается от Intel/AT&T. Регистры без префикса (AX, не RAX). Операнды в обратном порядке от Intel.

Q17. Что такое NOSPLIT?

Директива в asm-функции: убрать stack growth check в прологе. Для очень маленьких функций (< 96 байт frame).

Q18. Что такое //go:noescape?

Директива для asm-функции без Go-тела. Говорит компилятору: аргументы не escape’ят, можно оставить на стеке у caller’а.

Q19. Что такое //go:linkname?

Linker-trick: ссылка на private символ другого пакета. Используется в stdlib (atomic ← runtime, time ← runtime). С Go 1.23+ запрещено вне stdlib.

Q20. Зачем import _ "unsafe" при использовании linkname?

Это требование компилятора. Linkname считается “unsafe” фичей.

Q21. Что такое intrinsics?

Функции, которые компилятор знает специально — заменяет их вызов на инлайн machine code. Примеры: atomic.AddInt64, memmove, hash functions, math.Sqrt.

Q22. Где SIMD используется в Go stdlib?

В crypto: chacha20, sha256 (SHA-NI), aes (AES-NI). Файлы *_amd64.s, *_arm64.s.

Q23. Какой overhead у interface call?

Обычно ~5 ns + потенциальный cache miss. Devirtualization (Go 1.19+) убирает это, если возможно.

Q24. Что такое //go:noinline и зачем?

Запрет inlining конкретной функции. Используется:

  • В бенчмарках (чтобы measured function не была inlined).
  • Для debugging (видеть в stack trace).
  • Иногда для contrlling escape (inline может escape arguments).

Q25. Как сделать static binary с CGO?

Окно терминала
CGO_ENABLED=1 \
go build -ldflags '-extldflags "-static"' .

Работает с musl (Alpine). С glibc — много проблем (DNS resolver и т.д.).

Альтернатива: CGO_ENABLED=0 если код позволяет.


Возьми функцию из любого open-source Go проекта (например, gin web framework). Прогон с -gcflags="-m=2". Найди:

  • Какие функции inlined.
  • Какие не inlined и почему.
  • Какие переменные escape.

Напиши функцию sum(s []int) int. Прогон с -gcflags="-d=ssa/check_bce/debug=1". Если есть BC — перепиши, чтобы убрать (через “hint” _ = s[len(s)-1]).

Возьми любой web service. Сними CPU profile под нагрузкой (через go test -bench или wrk). Build с PGO. Сравни throughput.

Реализуй модуль с двумя реализациями:

  • feature_basic.go (//go:build !premium)
  • feature_premium.go (//go:build premium)

Покажи разный binary size с -tags="premium" и без.

Напиши Go-программу, которая вызывает C-функцию через CGO. Замерь latency (time.Now() до и после CGO call) на 1M итераций. Объясни overhead.

Напиши пример с C-callback’ом, который ожидает Go object. Используй cgo.NewHandle. Покажи, что без Handle Go-объект может быть собран GC.

Напиши func Add(a, b int64) int64 в Plan 9 asm для amd64 (add_amd64.s). Замерь bench и сравни с Go-реализацией. (Спойлер: оба будут ~1 ns, потому что один такт.)

Используй //go:linkname для доступа к runtime.nanotime. Сравни с time.Now().UnixNano(). Замерь overhead.

⚠️ С Go 1.23+ это может не сработать без специальных флагов. Может потребоваться //go:linkname runtime trampoline.


  1. src/cmd/compile/ — исходники компилятора.
  2. src/cmd/compile/internal/ssa/ — SSA passes (включая BCE, inlining, devirt).
  3. src/cmd/compile/internal/escape/ — escape analysis.
  4. src/runtime/cgocall.go — CGO runtime support.
  5. src/runtime/asm_amd64.s — основной runtime assembly.
  6. “Profile-guided optimization” — Go 1.21 release notes / Go blog.
  7. Keith Randall, “Go assembly cheat sheet” — wiki.
  8. Russ Cox, “Plan 9 from User Space: Assembler” — оригинальный design.
  9. “Cgo” — golang.org/cmd/cgo.
  10. Dave Cheney, “cgo is not Go” — blog post.
  11. “Go’s compiler intrinsics” — Vincent Blanchon, Medium 2020.
  12. “Go assembly by example” — github.com/teh-cmc/go-internals.
  13. Eli Bendersky, “Implementing FizzBuzz in Go assembly” — blog.
  14. “PGO in Go: real-world results” — Cloudflare blog 2024.
  15. “Replacing CGO with pure Go” — Tinkoff Engineering blog 2024.