2. Установка Go, GOPATH, GOROOT и первая программа

Установка среды Go на современную операционную систему предельно тривиальна: достаточно скачать официальный дистрибутив или воспользоваться пакетным менеджером платформы (brew install go на macOS или apt install golang в Linux). Любой опытный инженер за минуту пропишет путь к исполняемым файлам в переменную окружения PATH.

Однако за этой кажущейся простотой скрывается особая организация рабочего пространства и файловой системы. Понимание взаимодействия компилятора с путями на диске — это не просто теоретический экскурс, а базовое требование для прохождения архитектурных собеседований, настройки CI/CD-пайплайнов и сборки легковесных Docker-образов.


Две главные переменные: GOROOT и GOPATH

Когда тулчейн (toolchain) Go приступает к сборке проекта, ему требуется однозначно разрешить два вопроса:

  1. Где физически расположены бинарники самого языка, компилятор и стандартная библиотека?
  2. Где находятся исходники ваших собственных проектов и кэш сторонних модулей?

За эти зоны ответственности исторически отвечают две системные переменные окружения — GOROOT и GOPATH.

GOROOT: Дом для самого Go

Переменная GOROOT указывает на системный каталог, куда распакован дистрибутив языка Go (по умолчанию это /usr/local/go в Linux/macOS или C:\Program Files\Go в Windows).

Внутри этого каталога находится неизменяемый фундамент платформы:

  • bin/ — исполняемые бинарники компилятора (go), форматировщика исходников (gofmt) и вспомогательных утилит;
  • src/ — полные исходные тексты стандартной библиотеки (пакеты fmt, net/http, os, runtime и многие другие);
  • pkg/ — служебные файлы тулчейна: tool/ (компилятор, линкер, vet) и include/. До Go 1.19 здесь лежали и предкомпилированные архивы (.a) стандартной библиотеки; с Go 1.20 она не поставляется предкомпилированной: пакеты собираются по требованию и кэшируются в кэше сборки (GOCACHE).

GOPATH: Историческое наследие и современность

Переменная GOPATH определяет единое рабочее пространство (Workspace) разработчика.

Язык Go создавался внутри поискового гиганта Google, где весь исходный код компании исторически хранится в едином колоссальном монорепозитории. Ранние версии Go (вплоть до версии 1.10) унаследовали эту монолитную философию: инструмент ожидал, что абсолютно все ваши проекты, утилиты и скачанные сторонние библиотеки будут собраны под одной корневой директорией (по умолчанию ~/go).

Классический GOPATH строго делился на три подкаталога:

  1. src/ — исходники всех ваших проектов и внешних репозиториев (например, ~/go/src/github.com/user/project);
  2. pkg/ — скомпилированные промежуточные объектные файлы (.a);
  3. bin/ — готовые исполняемые бинарники ваших программ и инструментов.

Фатальная проблема старого подхода: В GOPATH-режиме (он оставался режимом по умолчанию до Go 1.15 включительно) у каждого пакета могла быть только одна версия на всё рабочее пространство. Если проекту A требовалась версия v1.0.0, а проекту B — v2.0.0, возникал версионный конфликт в папке ~/go/src/. Штатного решения не было: его обходили каталогом vendor/ внутри проекта или отдельным GOPATH на каждый проект.

Эпоха Go Modules (Наше время)

Начиная с релиза Go 1.11 (а по умолчанию — с Go 1.16, когда GO111MODULE стал равен on), язык перешел на современную децентрализованную систему версионирования — Go Modules.

Теперь разработчик не обязан держать свои исходные коды внутри каталога GOPATH/src. Вы можете создать каталог проекта в любой точке файловой системы и инициализировать изолированный модуль командой go mod init. (Все тонкости работы модульной системы мы детально исследуем в лекции 28. Модули и go.mod).

flowchart TD
    GOPATH["Ваш GOPATH<br>(по умолчанию ~/go)"]
    GOPATH --> BIN["/bin<br>Глобальные исполняемые утилиты<br>(go install ...)"]
    GOPATH --> PKG["/pkg<br>Кэш модулей (pkg/mod)"]
    GOPATH --> SRC["/src<br>Legacy-код<br>(до эпохи Go 1.11)"]

    PKG --> MOD["/mod<br>Кэш скачанных библиотек<br>(github.com/gin-gonic/... и др.)"]
    PKG --> SUM["/sumdb<br>Кэш контрольных сумм модулей"]

    style GOPATH fill:#1e293b,stroke:#818cf8,stroke-width:2px,color:#e2e8f0
    style BIN fill:#132e1b,stroke:#22c55e,stroke-width:2px,color:#bbf7d0
    style PKG fill:#1e1e38,stroke:#38bdf8,stroke-width:2px,color:#e2e8f0
    style SRC fill:#2d1515,stroke:#ef4444,stroke-width:2px,color:#fecaca
  

Первая программа

Создадим отдельную директорию в любом удобном каталоге файловой системы (вне устаревшего пути GOPATH/src), например ~/projects/hello-go.

Создадим файл main.go:

Go
package main

import (
    "fmt"
    "runtime"
)

func main() {
    fmt.Println("Hello, System!")
    fmt.Printf("OS: %s\nArch: %s\n", runtime.GOOS, runtime.GOARCH)
}

Разбор структуры

  1. package main — фундаментальная декларация пакета. Она сообщает компилятору, что данный файл предназначен для сборки исполняемого бинарного файла, а не разделяемой библиотеки. Компилятор Go сгенерирует точку входа только при наличии пакета main и функции main().
  2. import — блок явного подключения зависимостей. Компилятор Go бескомпромиссен: если пакет импортирован, но ни один его идентификатор не используется в тексте файла, сборка завершится с ошибкой. Это исключает попадание мертвого кода в исполняемый файл.
  3. func main() — системная точка входа (Entry point). В Go функция main() не принимает параметров (аргументы командной строки считываются через срез os.Args) и не возвращает код завершения (код выхода операционной системе передается через вызов os.Exit()).

Компиляция и Mechanical Sympathy

Тулчейн Go предлагает две базовые команды для работы с кодом:

  • go run — компилирует программу во временный каталог операционной системы и немедленно передает управление на исполнение;
  • go build — генерирует готовый бинарный артефакт в текущей рабочей директории.

Выполним прямую компиляцию:

Bash
go build -o app main.go
./app

Что мы получили на диске?
В отличие от виртуальных сред Java или платформы .NET, на выходе создается нативный бинарный исполняемый файл платформы: формат ELF в Linux, Mach-O в macOS или PE в Windows.

Исследуем полученный файл с помощью стандартной Unix-утилиты (в окружении Linux):

Bash
file ./app
# Вывод: app: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=..., not stripped

Обратите внимание на ключевую характеристику: statically linked (или dynamically linked при определенных флагах).

Влияние CGO_ENABLED

В Go встроен мощный мост взаимодействия с библиотеками на языке Си — инструмент cgo. По умолчанию он включён (CGO_ENABLED=1), если в системе найден C-компилятор; с Go 1.20 без компилятора в PATH (и при кросс-компиляции) cgo по умолчанию выключен. Если программа или ее внутренние зависимости (например, сетевой пакет net или os/user) задействуют возможности Си, результирующий бинарник будет динамически слинкован с системной библиотекой libc вашей текущей операционной системы (например, glibc в Ubuntu).

Чем это грозит серверному инженеру?
Если вы соберете бинарник на машине под управлением Ubuntu (где используется glibc) и скопируете его в ультралегкий контейнер Docker на базе Alpine Linux (где используется альтернативная библиотека musl libc), программа откажется запускаться с загадочной ошибкой file not found. Ядро Linux просто не сможет обнаружить динамический загрузчик ld-linux.so, зашитый в заголовок ELF-файла.

Чтобы гарантировать создание истинно автономного, на 100% статически слинкованного артефакта (который запускается даже в пустом Docker-образе scratch, где нет ничего, кроме системных вызовов ядра), собирайте проект с отключенным cgo:

Bash
CGO_ENABLED=0 go build -o app main.go

При флаге CGO_ENABLED=0 компилятор Go подменяет системные вызовы Си (например, резолвер DNS) собственными реализациями на чистом Go (pure Go). Бинарник становится полностью самодостаточным: на Linux он взаимодействует с ядром операционной системы напрямую через системные вызовы (syscalls), игнорируя внешние динамические библиотеки. (На macOS и OpenBSD Go и без cgo обращается к ядру через системную libc, на Windows — через системные DLL.)


Итог

  1. GOROOT — каталог с компилятором и стандартной библиотекой языка. В 99% случаев определяется автоматически.
  2. GOPATH — в эпоху Go Modules служит глобальным кэшем модулей (GOPATH/pkg/mod) и каталогом для установленных утилит командной строки (GOPATH/bin).
  3. Исполняемое приложение всегда берет начало с директивы package main и точки входа func main().
  4. Использование переменной CGO_ENABLED=0 при вызове go build на Linux создает полностью статически слинкованный бинарник, идеальный для микроконтейнеров scratch.

В следующей лекции 3. go run, go build, go test и базовый toolchain мы детально исследуем встроенный инструментарий платформы: разберем флаги компилятора, детектор гонок данных (race detector) и запуск тестов без внешних систем сборки вроде Make или CMake.