Збірка, кодогенерація та cgo¶
Як програма на Go стає артефактом для розгортання: згенероване джерело, теги збірки, штампування версії, крос-компіляція й та одна річ, яка ускладнює все це — cgo.
go:generate¶
Коментар //go:generate записує команду. go generate знаходить їх і
запускає; go build — ніколи:
Це розділення свідоме: генерація — це крок, який ви робите й комітите, а не те, що відбувається на кожній збірці. Будь-хто, хто клонує репозиторій, може зібрати проєкт без встановленого генератора.
Директива може запустити що завгодно — генератор коду, компілятор схеми, інструмент шаблонів. Вона виконується в каталозі файлу з оточенням пакета.
Багато проєктів пропускають go:generate і запускають генерацію з
Makefile натомість, що тримає кожен крок збірки в одному видимому
місці, а не розкиданим по вихідних файлах. Обидва підходи прийнятні;
змішування їх означає, що ніхто не знає, що саме що виробило.
Згенеровані файли — лише для читання¶
Згенерований код на Go несе стандартний перший рядок:
Саме ця форма — що відповідає ^// Code generated .* DO NOT EDIT\.$ —
розпізнається інструментами. Лінтери пропускають ці файли, а
рецензенти знають, що коментувати їх не варто.
Комітьте згенеровані файли. Це тримає збірку відтворюваною без генератора, робить diff видимим у рев'ю, і означає, що CI не потребує встановленого інструмента. Ціна — пам'ятати про регенерацію; перевірка CI, що регенерує й провалюється на diff, закриває цю прогалину.
Теги збірки¶
Рядок //go:build на початку файлу вирішує, чи компілювати його
взагалі. Він має стояти перед секцією package, з порожнім рядком
після нього:
Два файли, один ідентифікатор, і потрібний вибирається під кожну ціль:
$ go list -f '{{.GoFiles}}' .
[only_other.go pill.go]
$ GOOS=linux go list -f '{{.GoFiles}}' .
[only_linux.go pill.go]
go list — швидкий спосіб перевірити, що насправді обирає комбінація
тегів — здогадки — це те, як ви отримуєте файл, який ніщо не
компілює.
Суфікси імен файлів роблять те саме неявно: foo_linux.go,
foo_windows.go, foo_amd64.go та foo_test.go. Надавайте перевагу
суфіксу для варіантів платформи; використовуйте явний тег для всього
іншого.
Не ховайте тести за власним тегом¶
//go:build integration на тестовому файлі означає, що він
запускається лише з -tags integration. Якщо ні CI, ні локальний
make test не передають цей прапорець, тести не запускаються ніде
— і при цьому весь час виглядають присутніми в репозиторії, що гірше,
ніж якби їх не було.
Замість цього ховайте на етапі виконання, де пропуск видимий:
func TestAgainstDatabase(t *testing.T) {
dsn := os.Getenv("TEST_DATABASE_DSN")
if dsn == "" {
t.Skip("TEST_DATABASE_DSN not set")
}
// ...
}
Тоді go test -v друкує пропуск, тож відсутність повідомляється, а не
замовчується.
Штампування версії через -ldflags¶
-X встановлює рядкову змінну на етапі лінкування — саме так бінарник
дізнається власну версію без згенерованого файлу:
Це працює лише з рядковою (string) змінною на рівні пакета без
ініціалізатора, окрім константи. -ldflags "-s -w" додатково прибирає
таблицю символів і дані DWARF, даючи помітно менший бінарник — ціною
читабельних трасувань стека.
runtime/debug.ReadBuildInfo() дає вам версії модулів і інформацію
про VCS, яку toolchain записує автоматично, що покриває багато
випадків узагалі без прапорців.
Крос-компіляція¶
Встановіть дві змінні. Жодного toolchain для встановлення, жодного контейнера не потрібно:
GOOS=linux GOARCH=amd64 go build -o app-linux-amd64 ./cmd/api
GOOS=linux GOARCH=arm64 go build -o app-linux-arm64 ./cmd/api
GOOS=windows GOARCH=amd64 go build -o app-windows.exe ./cmd/api
GOOS=darwin GOARCH=arm64 go build -o app-darwin-arm64 ./cmd/api
Усі чотири збираються з однієї машини. go tool dist list друкує
кожну підтримувану пару.
Інші прапорці, які варто знати: -trimpath прибирає локальні шляхи
файлової системи з бінарника, що потрібно для відтворюваних збірок і
щоб не витікав ваш домашній каталог у трасування стека.
Контейнери¶
Оскільки результат — це статичний бінарник, образ може бути майже порожнім:
FROM golang:1.27 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o /out/app ./cmd/api
FROM gcr.io/distroless/static-debian12
COPY --from=build /out/app /app
ENTRYPOINT ["/app"]
Копіювання go.mod/go.sum і запуск go mod download перед рештою
джерела — це трюк кешування шарів: залежності перезавантажуються лише
тоді, коли ці два файли змінюються.
CGO_ENABLED=0 — це те, що дозволяє статичний (static) базовий
образ. З увімкненим cgo бінарнику потрібна libc, і фінальний етап має
її надати.
cgo¶
import "C" дозволяє Go викликати C. Так пакет прив'язується до
C-бібліотеки — tree-sitter, SQLite, кодеки зображень — і це змінює
правила:
- Крос-компіляція перестає бути безкоштовною. Тепер вам потрібен C-крос-компілятор для кожної цілі.
- Статичне лінкування стає складнішим, тож
distroless/staticвже не підходить. - Збірки повільніші, бо запускається C-компілятор.
- Пам'яттю керуєте ви самі. Збирач сміття Go не бачить виділень пам'яті з боку C, тож усе, що виділяє C-сторона, треба звільняти явно:
Забудьте цей Close, і ви отримаєте витік, який GC ніколи не забере, а
профіль купи pprof не покаже, бо ця пам'ять не в купі Go.
- Паніки не перетинають межу, і крах у C забирає весь процес без жодного трасування стека Go.
Тримайте CGO_ENABLED=0, доки щось справді цього не вимагає, і
запишіть цю вимогу в README — розробник, чия збірка раптом потребує
C-toolchain, заслуговує знати чому.
З досвіду Python:
go buildзамінює весь стек пакування — немає ні wheel, ні virtualenv, ні інтерпретатора для постачання. Теги збірки — це умовна компіляція, а не перевіркиsys.platformпід час виконання. cgo — це той самий компроміс, що й ctypes чи C-розширення, і коштує приблизно стільки ж.
Швидка довідка¶
| Задача | Форма |
|---|---|
| записати генератор | //go:generate cmd args, запуск через go generate ./... |
| позначити згенерований код | // Code generated by X. DO NOT EDIT. |
| умовний файл | //go:build tag перед package, порожній рядок після |
| варіанти платформи | суфікси _linux.go, _windows.go |
| перевірити, що обрано | go list -f '{{.GoFiles}}' . |
| ховати інтеграційні тести | змінна середовища й t.Skip — не тег збірки |
| проштампувати версію | -ldflags "-X main.version=1.2.3" |
| менший бінарник | -ldflags "-s -w", -trimpath |
| крос-компіляція | GOOS=… GOARCH=… go build |
| статичний бінарник для контейнера | CGO_ENABLED=0 |
| пам'ять C | defer x.Close(), завжди |
Джерела¶
go generate— go.dev/blog/generate- Build constraints — pkg.go.dev/cmd/go#hdr-Build_constraints
- Generated code convention — go.dev/s/generatedcode
go buildflags — pkg.go.dev/cmd/go#hdr-Compile_packages_and_dependenciesruntime/debug.ReadBuildInfo— pkg.go.dev/runtime/debug#ReadBuildInfo- cgo — pkg.go.dev/cmd/cgo