Олег Марков
Сборка проекта в Go Golang - практическое руководство
Введение
Сборка проекта в Go – это процесс превращения исходного кода в исполняемый бинарный файл или библиотеку. На первый взгляд кажется, что все сводится к одной простой команде go build. Но как только вы начинаете работать с реальными проектами, появляются вопросы: где будут лежать артефакты, как управлять зависимостями, что делать с разными платформами, как оптимизировать размер бинарника и скорость сборки.
В этой статье мы разберем, как устроен процесс сборки Go‑проекта, какие инструменты предоставляет стандартный тулчейн, как вы можете настраивать и автоматизировать сборку под свои задачи. Смотрите, я покажу вам, как шаг за шагом от простейшей команды перейти к полноценному процессу сборки для продакшн‑проекта.
Базовая модель сборки в Go
Что такое сборка в контексте Go
С точки зрения Go сборка – это:
- Анализ исходного кода и зависимостей.
- Компиляция пакетов в объектный код.
- Линковка объектных файлов в итоговый бинарный файл.
- (Опционально) кэширование результатов, чтобы ускорить последующие сборки.
При этом Go делает много вещей автоматически:
- сам находит зависимости по
import; - поднимает модульные зависимости из
go.mod; - кеширует промежуточные результаты;
- по умолчанию собирает статически связанный бинарник (за редкими исключениями).
Основные команды для сборки
Есть три базовые команды, которые вы будете использовать постоянно:
go build– собрать проект и получить бинарник;go install– собрать и установить бинарник/пакет в$GOBINили$GOPATH/bin;go run– скомпилировать и сразу запустить программу (без сохранения бинарника по умолчанию).
Давайте разберем их на простых примерах.
Пример минимального проекта
Пусть у вас есть такой проект:
myapp/
main.go
Файл main.go:
package main
import "fmt"
func main() {
// Здесь мы просто печатаем строку в стандартный вывод
fmt.Println("Hello, build!")
}
Теперь вы увидите, как это выглядит в терминале.
Сборка:
cd myapp
go build
По умолчанию:
- создается бинарный файл с именем каталога (
myappна Linux/macOS илиmyapp.exeна Windows); - бинарник появляется в текущей директории;
- зависимости подтягиваются автоматически (если используется модули и настроен
go.mod).
Запуск:
./myapp
# или на Windows
myapp.exe
Структура проекта и модули
Роль go.mod при сборке
Файл go.mod описывает модуль и его зависимости. С точки зрения сборки он важен тем, что:
- фиксирует версию Go (директива
go 1.xx); - определяет путь модуля (директива
module), который участвует в импортах; - хранит версии сторонних модулей (
require,replace).
Пример простого go.mod:
module github.com/username/myapp
go 1.22
require (
github.com/google/uuid v1.6.0 // Здесь мы добавили стороннюю зависимость
)
Когда вы вызываете go build, инструмент использует go.mod, чтобы:
- понять, какие версии библиотек скачивать;
- куда класть модуль в кеш;
- корректно резолвить импорты вида
github.com/username/myapp/pkg/....
Локальная структура каталогов
Чаще всего для приложений используют такую структуру:
myapp/
go.mod
main.go // или cmd/myapp/main.go
cmd/
myapp/
main.go
internal/
service/
service.go
pkg/
util/
util.go
При сборке:
go build ./...обойдет все пакеты в модуле;go build ./cmd/myappсоберет только исполняемый пакетmainвcmd/myapp.
Использовать cmd/ удобно, когда в одном модуле несколько бинарников.
Команда go build – подробный разбор
Базовый синтаксис
Общий вид:
go build [опции] [пакеты...]
Если пакеты не указаны, по умолчанию используется текущий каталог.
Примеры:
# Собрать текущий пакет
go build
# Собрать конкретный пакет
go build ./cmd/myapp
# Собрать все пакеты модуля (без установки)
go build ./...
# Собрать и задать имя выходного файла
go build -o bin/server ./cmd/server
Как go build находит точки входа
Go собирает исполняемый файл только из пакета main, в котором есть функция main. Если в пакете:
package main
func main() {
// Здесь код будет точкой входа программы
}
то go build создаст бинарник. Если это библиотечный пакет (например, package util), то по умолчанию бинарник не создается, но пакет компилируется, кешируется и может использоваться как зависимость.
Ключи и полезные опции go build
Давайте посмотрим на часто используемые флаги.
-o – имя выходного файла
go build -o build/myapp ./cmd/myapp
Здесь:
-o build/myapp– путь и имя бинарника;./cmd/myapp– пакет, который мы собираем.
Если вы не укажете -o, Go использует имя каталога пакета.
-v – подробный вывод
go build -v ./...
Флаг -v показывает, какие пакеты компилируются. Это помогает понять, что именно пересобирается, а что берется из кеша.
-race – детектор гонок
go build -race -o bin/myapp_race ./cmd/myapp
Флаг -race добавляет в бинарник детектор гонок по данным для многопоточного кода:
- бинарник получается больше и работает медленнее;
- но вы можете поймать скрытые проблемы с конкурентным доступом к памяти.
-tags – build tags
Build tags позволяют компилировать разные части кода в зависимости от условий (например, разные реализации для Linux и Windows, либо для debug и release вариантов).
Пример использования:
// +build debug
package config
// Здесь мы объявляем конфигурацию для debug сборок
var Mode = "debug"
и
// +build !debug
package config
// А здесь конфигурация по умолчанию для остальных сборок
var Mode = "release"
Сборка с тегом:
go build -tags debug ./cmd/myapp
Смотрите, я покажу вам, как это может пригодиться:
- подключать дополнительное логирование в debug‑сборках;
- включать мок‑реализации для тестов;
- собирать разные конфигурации под разные окружения.
Команда go install – чем она отличается от go build
Задача go install
go install не просто собирает код, а:
- компилирует пакет;
- кладет бинарный файл в
$GOBIN(если задан) или$GOPATH/bin(в модулях Go 1.20+ рекомендуется использовать GOBIN); - для библиотечных пакетов – устанавливает их в кеш компилятора.
Пример:
go install ./cmd/myapp
Если GOBIN установлен:
- бинарник попадает в
${GOBIN}/myapp; - вы можете запускать
myappиз любого места, если$GOBINесть вPATH.
Если вы укажете модуль с версией:
go install github.com/username/myapp/cmd/myapp@latest
Go:
- скачает модуль;
- соберет и установит бинарник в
GOBIN; - при этом исходники не будут помещены в текущий каталог.
Когда использовать go install
Используйте go install, когда:
- вы создаете CLI‑утилиту для себя/команды;
- хотите поставить сторонний инструмент (
golangci-lint,staticcheck); - делаете “глобальную” установку приложения.
Для локальной разработки чаще удобнее go build с -o.
Окружение для сборки: переменные и настройки
GOROOT, GOPATH и GOBIN
Сейчас у большинства проектов используется модульный режим, поэтому:
GOROOT– путь к установке Go (обычно не нужно трогать);GOPATH– рабочее пространство, по умолчанию~/go(на современных версиях Go его роль уменьшилась);GOBIN– куда ставятся бинарники черезgo install.
Проверка:
go env GOROOT
go env GOPATH
go env GOBIN
Рекомендация:
- явно задайте
GOBINи добавьте его вPATH, чтобы собранные утилиты были доступны из любого места.
Пример (bash):
export GOBIN=$HOME/go/bin
export PATH=$PATH:$GOBIN
GOOS и GOARCH – целевая платформа
Эти переменные управления сборкой определяют:
GOOS– операционная система (linux, windows, darwin и др.);GOARCH– архитектура (amd64, arm64 и др.).
По умолчанию Go собирает под текущую платформу. Но вы можете явно указать параметры для кросс‑компиляции:
# Собрать бинарник для Linux x86_64 на macOS
GOOS=linux GOARCH=amd64 go build -o bin/myapp-linux-amd64 ./cmd/myapp
На Windows задается с помощью set или через PowerShell:
$env:GOOS="linux"
$env:GOARCH="amd64"
go build -o bin\myapp-linux-amd64.exe .\cmd\myapp
Кросс-компиляция Go приложений
Как это работает
Go включает встроенный кросс‑компилятор для большинства комбинаций GOOS/GOARCH. Это значит, что вы можете собрать бинарник под Linux, находясь на macOS или Windows, без установки дополнительных компиляторов (в типичных случаях).
Пример практического сценария:
- вы разрабатываете на macOS;
- деплой – на Linux (Docker/Kubernetes);
- вы хотите собирать образы локально.
Сборка:
GOOS=linux GOARCH=amd64 go build -o build/app-linux-amd64 ./cmd/myapp
Распространенные комбинации GOOS/GOARCH
Несколько часто используемых:
GOOS=linux GOARCH=amd64– классический серверный Linux;GOOS=linux GOARCH=arm64– современные ARM‑сервера и некоторые контейнерные окружения;GOOS=windows GOARCH=amd64– Windows 64‑бит;GOOS=darwin GOARCH=arm64– macOS на Apple Silicon;GOOS=darwin GOARCH=amd64– macOS на Intel (или бинарник для Rosetta).
Чтобы посмотреть все поддерживаемые комбинации:
go tool dist list
Ограничения кросс-компиляции
Не всякий код успешно собирается под любую платформу. Обратите внимание:
- системные вызовы (
syscall) и специфичные API могут быть доступны только на части платформ; - natively скомпилированные C‑библиотеки через cgo усложняют кросс‑компиляцию (может потребоваться соответствующий кросс‑C‑компилятор);
- файловые пути, разрешения и особенности работы с сетью могут отличаться.
Если вы используете cgo, обязательно проверяйте:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build ./cmd/myapp
Если сборка не проходит, посмотрите, какие C‑зависимости нужны.
Оптимизация сборки: размер, скорость, повторяемость
Оптимизация размера бинарника
Go по умолчанию генерирует статически связанный бинарник, который уже неплохо оптимизирован. Но можно сделать его еще меньше.
Самые частые приемы:
go build -ldflags="-s -w" -o bin/myapp ./cmd/myapp
Здесь:
-s– убрать символы отладки;-w– убрать таблицы отладочной информации.
Чтобы еще больше уменьшить бинарник на Linux, используют upx, но это нужно делать осторожно:
upx --best bin/myapp
Помните, что:
- после сжатия сложнее анализировать аварийные дампы;
- иногда это может мешать некоторым средствам безопасности.
Встраивание версии и других метаданных
Частая задача – “прошить” в бинарник номер версии, commit hash, дату сборки. Покажу вам, как это реализовано на практике.
Создадим пакет с переменными:
// pkg/buildinfo/buildinfo.go
package buildinfo
// Здесь мы объявляем переменные, значения которым зададим при сборке через -ldflags
var (
Version = "dev"
Commit = "none"
BuildDate = "unknown"
)
Используем его в main:
// cmd/myapp/main.go
package main
import (
"fmt"
"github.com/username/myapp/pkg/buildinfo"
)
func main() {
// Здесь мы печатаем информацию о версии при запуске
fmt.Printf("Version: %s, Commit: %s, BuildDate: %s\n",
buildinfo.Version, buildinfo.Commit, buildinfo.BuildDate)
}
Теперь сборка:
go build -ldflags "\
-X 'github.com/username/myapp/pkg/buildinfo.Version=1.2.3' \
-X 'github.com/username/myapp/pkg/buildinfo.Commit=$(git rev-parse --short HEAD)' \
-X 'github.com/username/myapp/pkg/buildinfo.BuildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)'" \
-o bin/myapp ./cmd/myapp
Комментарии к ключу -X:
- он задает значение экспортируемой строковой переменной в момент линковки;
- путь указывается полностью – от имени модуля до переменной.
Так вы получаете воспроизводимую информацию о том, какая именно версия бинарника запущена.
Повторяемая сборка
Повторяемая сборка (reproducible build) – это когда один и тот же исходный код и набор параметров всегда дают побитово идентичный бинарник. Для Go это уже почти так, но на результат могут влиять:
- время сборки (если вы его вшиваете через
-ldflags); - случайный порядок итерации по мапам в генераторах кода (если вы их используете);
- зависимость от системных утилит.
Чтобы сделать сборку более повторяемой:
- используйте фиксированные версии зависимостей в
go.mod; - не вшивайте текущую дату/время без необходимости или задавайте их явно от CI;
- избегайте генерации переменного кода на лету в процессе сборки.
Сборка в разных окружениях: dev, test, prod
Подходы к конфигурации
Есть несколько типичных способов управлять конфигурацией при сборке:
- Build tags (
-tags). - Переменные окружения (читаются в рантайме).
- Значения, “прошитые” через
-ldflags.
Давайте разберемся на примере с окружениями dev и prod.
Вариант 1 – build tags
Создадим два файла:
// config_dev.go
//go:build dev
package config
// Здесь мы объявляем конфигурацию для dev сборки
var Env = "dev"
var LogLevel = "debug"
// config_prod.go
//go:build !dev
package config
// А здесь конфигурация по умолчанию для остальных сборок (prod)
var Env = "prod"
var LogLevel = "info"
Используем:
// main.go
package main
import (
"fmt"
"github.com/username/myapp/internal/config"
)
func main() {
// Здесь мы печатаем активное окружение
fmt.Printf("Running in %s environment with log level %s\n",
config.Env, config.LogLevel)
}
Сборка:
# dev
go build -tags dev -o bin/myapp-dev
# prod (без тега dev)
go build -o bin/myapp-prod
Вариант 2 – переменные окружения
Можно не разделять код по разным файлам, а читать окружение в рантайме:
package config
import "os"
var (
Env = getEnv("APP_ENV", "dev")
LogLevel = getEnv("APP_LOG_LEVEL", "debug")
)
func getEnv(key, defaultValue string) string {
// Здесь мы пытаемся получить переменную окружения и подставляем значение по умолчанию
if v := os.Getenv(key); v != "" {
return v
}
return defaultValue
}
Тогда одна и та же сборка работает по‑разному в зависимости от переменных среды.
Интеграция сборки с тестами и статическим анализом
Сборка и тестирование: go test
Команда go test на самом деле тоже выполняет сборку:
- она компилирует тесты вместе с кодом;
- запускает тестовый бинарник;
- удаляет его после выполнения (по умолчанию).
Пример:
go test ./...
Здесь:
./...– все пакеты вашего модуля;- в каждом пакете ищутся файлы
*_test.go; - для каждого пакета создается временный тестовый бинарник.
Если сборка какого‑то пакета падает, вы увидите ошибку на этапе go test.
Статический анализ перед сборкой
Разумно добавить к процессу сборки этап статического анализа. Часто используют:
go vet– встроенный анализатор;- сторонние инструменты (
golangci-lint,staticcheckи др.).
Пример простого сценария:
go vet ./...
go test ./...
go build ./cmd/myapp
Можно оформить это в отдельный скрипт, чтобы запускать одной командой.
Автоматизация сборки: Makefile и скрипты
Makefile для Go проекта
Хотя Go самодостаточен, удобно описать типичные команды в Makefile. Давайте посмотрим, что происходит в следующем примере.
Создадим файл Makefile:
# Здесь мы объявляем имя бинарника
BINARY_NAME := myapp
# Путь к основному пакету
MAIN_PKG := ./cmd/myapp
# Директория для бинарников
BIN_DIR := bin
.PHONY: build clean run test
build:
# Здесь мы собираем бинарник в директорию bin
GOOS=linux GOARCH=amd64 go build -o $(BIN_DIR)/$(BINARY_NAME) $(MAIN_PKG)
run:
# Здесь мы запускаем уже собранный бинарник
./$(BIN_DIR)/$(BINARY_NAME)
test:
# Здесь мы запускаем все тесты проекта
go test ./...
clean:
# Здесь мы удаляем бинарники
rm -rf $(BIN_DIR)
Использование:
make build
make test
make run
make clean
Так вы стандартизируете процесс для всей команды: все используют одинаковые цели make, независимо от IDE.
Скрипты на bash/PowerShell
Если Make не подходит (например, на Windows без WSL), можно использовать скрипты.
Пример build.sh:
#!/usr/bin/env bash
set -euo pipefail
# Здесь мы задаем значение по умолчанию для окружения
env="${1:-dev}"
echo "Building for env=${env}"
ldflags="-s -w"
if [ "$env" = "prod" ]; then
# Здесь мы добавляем дополнительные флаги для prod сборки
ldflags="$ldflags -X 'github.com/username/myapp/pkg/buildinfo.Version=1.0.0'"
fi
# Здесь мы запускаем go build с нужными флагами
GOOS=linux GOARCH=amd64 go build \
-ldflags "$ldflags" \
-o bin/myapp-$env \
./cmd/myapp
Запуск:
chmod +x build.sh
./build.sh dev
./build.sh prod
Отладочные и релизные сборки
Debug‑сборки
Для отладки удобно:
- не отключать символы отладки (
без -s -w); - включать дополнительное логирование;
- иногда включать
-race.
Пример:
go build -race -tags dev -o bin/myapp-debug ./cmd/myapp
Здесь:
-raceвключает детектор гонок;-tags devвключает debug‑конфигурацию;- бинарник называется
myapp-debug.
Release‑сборки
Для продакшн‑сборки цель иная:
- минимальный размер;
- встроенная версия;
- сборка под нужную платформу.
Пример команды:
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \
go build -ldflags "\
-s -w \
-X 'github.com/username/myapp/pkg/buildinfo.Version=1.0.0' \
-X 'github.com/username/myapp/pkg/buildinfo.Commit=$(git rev-parse --short HEAD)'" \
-o bin/myapp-linux-amd64 \
./cmd/myapp
Обратите внимание:
CGO_ENABLED=0отключает использование cgo, что помогает делать более переносимые статические бинарники для контейнеров;-s -wуменьшает размер файла;-X ...вшивает метаданные версии.
Типичные ошибки при сборке и как их разбирать
cannot find module providing package …
Когда go build пишет:
cannot find module providing package github.com/username/somepkg
это означает, что:
- в
go.modнет нужной зависимости; - или модуль недоступен по сети;
- или вы работаете вне модульного каталога.
Решение:
# Добавить зависимость
go get github.com/username/somepkg@latest
# Обновить и почистить go.mod/go.sum
go mod tidy
undefined reference / undefined: symbol
Ошибка вида:
undefined: SomeFunc
говорит, что:
- компилятор не может найти реализацию символа;
- возможно, вы забыли импортировать пакет;
- или функция имеет другое имя/подпись;
- или файл с реализацией исключен из сборки build tags.
Проверьте:
- импорты;
- файлы с директивами
//go:build/// +build; - регистр (Go чувствителен к регистру идентификаторов).
incompatible types / mismatched types
Ошибки несовместимости типов появляются и на этапе сборки, и на этапе тестов. Это нормально: Go специально проверяет много вещей на этапе компиляции.
Чтобы разобраться:
- внимательно читайте, какие типы ожидаются и какие передаются;
- проверьте сигнатуру функций и интерфейсов;
- просмотрите последние изменения (часто проблема в недавно отредактированном коде).
Заключение
Сборка проекта в Go устроена так, чтобы по умолчанию быть максимально простой: достаточно go build, чтобы получить готовый бинарник. Но по мере роста проекта требований становится больше: нужно управлять зависимостями, окружениями, платформами, встраивать метаданные версии, автоматизировать процесс, разделять debug и release сборки.
Вы посмотрели, как:
- работает базовая команда
go buildи чем она отличается отgo install; - использовать
GOOS,GOARCHиCGO_ENABLEDдля кросс‑компиляции; - управлять конфигурацией через build tags, переменные окружения и
-ldflags; - оптимизировать размер бинарника и встраивать в него версию и commit;
- подключить сборку к тестам и статическому анализу;
- автоматизировать процесс через Makefile и скрипты;
- разбирать типичные ошибки сборки.
Этого достаточно, чтобы выстроить в проекте понятный и воспроизводимый процесс сборки, который удобно поддерживать всей командой и развивать по мере роста системы.
Частозадаваемые технические вопросы по теме статьи
1. Как собрать Go проект в Docker образе максимально небольшого размера
Один из удобных способов – использовать multi-stage build.
Пример Dockerfile:
# Первый этап - сборка
FROM golang:1.22-alpine AS builder
WORKDIR /app
# Здесь мы копируем go.mod и go.sum и устанавливаем зависимости
COPY go.mod go.sum ./
RUN go mod download
# Здесь мы копируем остальной код
COPY . .
# Здесь мы собираем статический бинарник
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags="-s -w" -o /app/myapp ./cmd/myapp
# Второй этап - минимальный образ
FROM scratch
# Здесь мы копируем только бинарник
COPY --from=builder /app/myapp /myapp
# Здесь мы задаем команду по умолчанию
ENTRYPOINT ["/myapp"]
Так вы получаете образ, в котором только один бинарник без лишних слоев.
2. Как собрать несколько бинарников из одного модуля одной командой
Если у вас несколько пакетов cmd/*, сделайте цель в Makefile:
BIN_DIR := bin
.PHONY: build-all
build-all:
# Здесь мы собираем все команды из каталога cmd
for dir in $$(find cmd -mindepth 1 -maxdepth 1 -type d); do \
name=$$(basename $$dir); \
echo "Building $$name..."; \
go build -o $(BIN_DIR)/$$name ./cmd/$$name; \
done
Запуск make build-all соберет бинарник для каждого подпакета в cmd/.
3. Как включить cgo только для части кода и не ломать кросс-компиляцию
Разделите реализацию по файлам с build tags.
Пример:
// db_cgo.go
//go:build cgo
package db
/*
#include <some_c_lib.h>
*/
import "C"
// Здесь мы объявляем реализацию с использованием cgo
func Connect() error {
// ...
return nil
}
// db_pure.go
//go:build !cgo
package db
// Здесь мы объявляем реализацию без cgo
func Connect() error {
// ...
return nil
}
Сборка:
- с cgo:
go build -tags cgo ./...; - без cgo:
CGO_ENABLED=0 go build ./....
4. Как собрать тесты в бинарник, но не запускать их сразу
Иногда нужно получить тестовый бинарник отдельно (например, для запуска в другом окружении).
Используйте:
go test -c -o bin/myapp.test ./...
Опция -c говорит go test “скомпилировать, но не запускать”. Полученный бинарник можно запустить вручную.
5. Как сделать разные точки входа, но общую бизнес-логику
Вынесите бизнес‑логику в отдельный пакет, а в cmd/* оставьте только main.
Структура:
internal/
app/
app.go
cmd/
api/
main.go
worker/
main.go
internal/app/app.go:
package app
// Здесь мы описываем общую бизнес-логику
func RunAPI() error {
// ...
return nil
}
func RunWorker() error {
// ...
return nil
}
cmd/api/main.go:
package main
import "github.com/username/myapp/internal/app"
func main() {
// Здесь мы запускаем API
_ = app.RunAPI()
}
cmd/worker/main.go:
package main
import "github.com/username/myapp/internal/app"
func main() {
// Здесь мы запускаем воркер
_ = app.RunWorker()
}
Сборка:
go build -o bin/api ./cmd/api
go build -o bin/worker ./cmd/worker
Постройте личный план изучения Vue до уровня Middle — бесплатно!
Vue — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Бесплатные лекции
Все гайды по Vue
Лучшие курсы по теме

Vue 3 и Pinia
Антон Ларичев
TypeScript с нуля
Антон Ларичев