1.8Модули, пакеты, инструментарий
Проследим, как Go выбирает версии модулей и почему пакет, модуль и репозиторий имеют разные границы.
Модуль, пакет, репозиторий: три разные вещи
| Пакет | Модуль | |
|---|---|---|
| Что это | Единица компиляции и области видимости | Единица версионирования и распространения |
| Границы | Одна директория (без вложенных) | Дерево директорий от go.mod вниз |
| Объявляется | package name в каждом файле |
go.mod в корне |
| Имя | Короткое: http, json |
Путь: github.com/org/repo |
| Видимость | Экспорт по заглавной букве | Ограничение через internal/ |
| Импортируется | Да, по полному пути | Нет - импортируют пакеты внутри него |
| Версия | Нет | Есть, semver |
| Сколько | Много в модуле | Обычно один на репозиторий |
Формулировка на собес: модуль объединяет пакеты, которые версионируют и выпускают вместе. Импортируешь всегда пакет, а не модуль; путь импорта складывается из пути модуля и пути директории внутри него. Один репозиторий может держать несколько модулей (монорепо с подмодулями), но тогда начинаются теги вида subdir/v1.2.0 и ручная синхронизация версий, поэтому так делают редко.
go.mod, границу пакета задаёт директория. Путь импорта склеивается из пути модуля и относительного пути папки.
go.mod: что там лежит
module github.com/acme/billing // путь модуля = префикс всех путей импорта
go 1.24.0 // языковая версия: какие фичи разрешены и какая семантика
toolchain go1.24.3 // какой тулчейн скачать, если локальный младше (1.21+)
require (
github.com/jackc/pgx/v5 v5.6.0 // прямые зависимости
go.uber.org/zap v1.27.0
)
require (
github.com/pkg/errors v0.9.1 // indirect // косвенные: нужны зависимостям
)
replace github.com/broken/lib => ../local-fork // подмена: путь или другая версия
exclude github.com/bad/lib v1.2.3 // «эту версию не брать никогда»
retract [v1.0.0, v1.0.4] // автор отзывает свои версии (1.16+)
tool golang.org/x/tools/cmd/stringer // Go 1.24: инструменты как зависимости
godebug default=go1.23 // Go 1.23: зафиксировать старое поведение
go задаёт не только «минимальную версию компилятора»
Она задаёт языковую версию для всех пакетов модуля: какие фичи доступны и по какой семантике всё работает. Самый громкий пример: go 1.22 включает новую семантику переменной цикла, а модуль с go 1.21 в том же билде продолжит работать по-старому. С Go 1.21 директива стала жёсткой. Тулчейн младше указанного не предупредит, а откажется собирать, и (при разрешённом GOTOOLCHAIN) сам скачает нужный. Ещё тонкость: указать go 1.24 и собирать старым компилятором нельзя. Наоборот можно и нужно: новый компилятор соберёт модуль с go 1.19.
replace, exclude и
retract
-
replaceдействует только в главном модуле. Оставишьreplaceв своей библиотеке, и у потребителей он просто проигнорируется: классическое «у меня работает, у них нет». Для локальной разработки лучшеgo work(workspaces, Go 1.18): делает то же самое, но живёт вgo.work, который не коммитят. -
excludeубирает конкретную версию из рассмотрения MVS. Дальше берётся следующее по величине требование, а оно может оказаться ниже исключённой версии, а не выше. Выкинешь битыйv1.4.0и уедешь наv1.1.0, хотяv1.9.0лежит рядом. Поэтомуexcludeобычно дополняют явнымrequireна здоровую версию. -
retractостаётся единственным способом «отозвать» уже опубликованный тег. Удалить версию из прокси нельзя (immutable), зато можно пометить её отозванной, иgo getперестанет её предлагать.
go.sum, checksum database и приватные модули
go.sum работает не как lock-файл. Версии фиксирует go.mod плюс алгоритм MVS, а go.sum отвечает за целостность. В нём лежат криптографические хеши (SHA-256, кодировка h1:) для каждой версии, которая когда-либо участвовала в сборке, включая те, что в итоге не выбраны.
# Две строки на каждый модуль:
github.com/jackc/pgx/v5 v5.6.0 h1:SWJze... # хеш дерева файлов модуля
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/... # хеш только его go.mod
Вторая строка нужна, чтобы считать граф зависимостей, не скачивая сами модули: для MVS хватает go.mod-файлов. Поэтому в go.sum регулярно оказываются модули, чей код в сборку не попал. Руками их не чисти, для этого есть go mod tidy.
| Переменная | Что делает | Когда трогать |
|---|---|---|
GOPROXY |
Список прокси через ,; direct -
идти в VCS напрямую. По умолчанию
https://proxy.golang.org,direct
|
Свой Athens/Nexus, закрытый контур |
GOSUMDB |
Прозрачный лог контрольных сумм, по умолчанию
sum.golang.org
|
off - только в закрытом контуре |
GONOSUMDB |
Шаблоны путей, для которых не сверяться с checksum
database. Сам go.sum при этом продолжает
работать
|
Обычно выставляется автоматически из
GOPRIVATE
|
GONOSUMCHECK |
Несуществующее имя: ни в vgo, ни в одном релизе Go
такой переменной не было, тулчейн её не читает
|
Если увидели в CI - менять на GOPRIVATE
|
GOPRIVATE |
Главная ручка: шаблоны путей, которые считаются
приватными. Автоматически задаёт GONOPROXY и
GONOSUMDB
|
GOPRIVATE=github.com/acme/* |
GONOPROXY |
Шаблоны, которые не идут через прокси | Тонкая настройка отдельно от GOPRIVATE |
GOFLAGS |
Флаги по умолчанию для всех go-команд |
GOFLAGS=-mod=mod в CI |
GOINSECURE |
Разрешить HTTP или невалидный TLS для шаблонов путей | Внутренний Git без нормального сертификата |
GOMODCACHE |
Где лежит кэш модулей, по умолчанию
$GOPATH/pkg/mod
|
Кэш в CI, отдельный диск |
GOTOOLCHAIN |
Разрешено ли автоматически скачивать тулчейн под директиву
go (Go 1.21+)
|
local - запретить скачивание в закрытом
контуре
|
Приватный репозиторий + дефолтные настройки = go mod download уходит в proxy.golang.org, получает 404, потом пытается сверить хеш с sum.golang.org и падает с verifying module: ... 404 Not Found. Лечится одной строкой: GOPRIVATE=github.com/acme/* сразу выключает и прокси, и sumdb для этих путей, плюс git config --global url."git@github.com:".insteadOf
"https://github.com/" для аутентификации. А вот выкручивать GOFLAGS=-mod=mod и GONOSUMDB=* «чтобы просто заработало» не стоит: так ты теряешь защиту от подмены и для публичных зависимостей тоже.
sum.golang.org держит append-only прозрачный лог (по типу Certificate Transparency) на дереве Меркла. Он берёт на себя то, чего go.sum не умеет: go.sum ловит подмену кода после того, как ты один раз зафиксировал хеш, а sumdb страхует первый раз и гарантирует, что все в мире видят один и тот же контент для этой версии. Отсюда и запрет «перевыпускать» тег. Автор сделал git tag -f v1.2.3, запушил другой код, новый хеш не сошёлся с записанным в логе, и сборка сломалась у всех, кто берёт модуль мимо прокси (прокси отдаёт старую копию): checksum mismatch ... SECURITY ERROR. Реагировать надо не командой go clean -modcache и не флагом GOFLAGS=-mod=mod, а разбирательством: либо кто-то переписал тег, либо в кэше или прокси лежит подменённый артефакт.
Semver и major-версии в пути импорта
Версии модулей подчиняются строгому semver: vMAJOR.MINOR.PATCH, обязательно с префиксом v. MAJOR меняется при ломающих изменениях, MINOR при обратно совместимых добавлениях, PATCH при исправлениях. А дальше правило, которого нет больше нигде:
«Если старый и новый пакет имеют один и тот же путь импорта, новый пакет должен быть обратно совместим со старым.» Отсюда следствие: несовместимость обязана менять путь. Поэтому начиная с v2 мажорная версия входит в путь модуля: github.com/acme/lib/v2. Версии v0 и v1 вынесли в исключение: у v0 гарантий совместимости нет вообще, а v1 оставили без суффикса ради совместимости со старыми путями импорта.
// Обе версии могут жить в одной программе одновременно: это разные модули
import (
pgx4 "github.com/jackc/pgx/v4"
pgx5 "github.com/jackc/pgx/v5"
)
// На практике большой сервис можно мигрировать по одному пакету за раз,
// а не «большим взрывом» в один коммит.
# Как выпускается v2 (два рабочих подхода)
# 1) Major subdirectory: копия кода в подпапке, работает даже со старыми тулчейнами
mkdir v2 && cp -r *.go v2/ && cd v2 && go mod init github.com/acme/lib/v2
git add . && git commit -m "v2" && git tag v2.0.0 && git push origin main v2.0.0
# 2) Major branch: отдельная ветка, где go.mod уже содержит /v2. Компактнее, но
# требует аккуратности с тегами
git checkout -b v2
go mod edit -module github.com/acme/lib/v2
# Внутренние импорты модуля тоже надо переписать на .../v2/...
git commit -am "v2" && git tag v2.0.0 && git push origin v2 v2.0.0
# Для модуля без тегов Go сам делает псевдоверсию:
# v0.0.0-20240115143022-a1b2c3d4e5f6
# base-версия UTC-время коммита первые 12 символов хеша
# Псевдоверсии сортируются по времени между собой. Но «всегда ниже тега» - миф:
# базовая версия зависит от того, есть ли тег выше по истории.
# тегов нет вообще -> v0.0.0-<время>-<хеш> (ниже любого релиза)
# есть тег v1.5.2 -> v1.5.3-0.<время>-<хеш> (выше v1.5.2, ниже v1.5.3)
# То есть псевдоверсия перебивает тег, от которого построена, и MVS выберет её.
# Версия v2+ модуля без go.mod получает суффикс +incompatible:
# github.com/old/lib v2.0.0+incompatible
-
Поставили тег
v2.0.0, но забыли дописать/v2вmodule.go getотказывается ставить и ругается: «.../lib@v2.0.0: invalid version: module contains a go.mod file, so module path must match major version (".../lib/v2")». -
Переименовали модуль в
/v2, а внутренние импорты оставили старыми. Тогда в бинарь приедут обе копии пакета, с двумя независимыми наборами глобальных переменных иinit(). -
Считают, что
v0.xзащищён semver. Нет: вv0ломать можно в любом минорном релизе, и MVS это не считает нарушением.
MVS - minimal version selection
Go выбирает версии не так, как npm, Cargo и pip. Всё правило умещается в одну фразу: для каждого модуля берётся максимум из минимально требуемых версий. Никакого солвера, никаких SAT-задач, никакого «взять самое свежее, что подходит под ^1.2.0».
# Инструменты, чтобы увидеть решение MVS
go list -m all # итоговый выбор версий (build list)
go mod graph # весь граф требований, ребро = "кто кого требует"
go mod why -m github.com/x/y # зачем этот модуль вообще в сборке: цепочка импортов
go list -m -u all # какие обновления доступны
go mod tidy # привести require и go.sum к тому, что реально импортируется
# Обновления всегда явные:
go get github.com/x/y@v1.5.0 # конкретная версия
go get -u ./... # поднять минорные и патчи всех зависимостей, включая косвенные
go get -u=patch ./... # только патчи
go get github.com/x/y@none # выкинуть из графа
Из-за «высокоточности»: набор версий определяет только содержимое go.mod всех участников графа. Ни дата сборки, ни состояние реестра, ни наличие lock-файла на него не влияют. Атака «опубликовать вредоносный патч-релиз и ждать, пока CI сам его подтянет» в Go не работает: пока никто не поднял требование, версия не приедет. Обратная сторона в том, что за CVE придётся следить самому, поэтому в проде рядом с MVS всегда стоят govulncheck и автоматический бампер.
GOPROXY, кэш модулей и вендоринг
Модуль скачивается по цепочке: локальный кэш → прокси из GOPROXY → VCS (direct). Кэш лежит в $GOPATH/pkg/mod (или GOMODCACHE), общий на всю машину. Внутри распакованные модули в режиме read-only плюс cache/download, то есть «зеркало прокси» в том же формате.
| Режим | Как включить | Плюсы | Минусы |
|---|---|---|---|
| Прокси по умолчанию | ничего не делать | Быстро, immutable, есть даже удалённые из GitHub модули | Внешняя зависимость CI от сети |
| Свой прокси (Athens, Nexus, JFrog) | GOPROXY=https://athens.corp,direct |
Работает в закрытом контуре, кэш внутри периметра, аудит | Надо поддерживать |
| Вендоринг | go mod vendor |
Сборка вообще без сети, код зависимостей в ревью и в диффе | Репозиторий пухнет, конфликты при мерже, надо не забывать пересобирать |
| Кэш в CI | кэшировать GOMODCACHE |
Простое ускорение, никаких изменений в репо | Кэш может протухнуть или испортиться |
go mod vendor # создаёт ./vendor с кодом зависимостей + vendor/modules.txt
# Дальше go build автоматически использует -mod=vendor, если:
# директория vendor/ существует и директива go >= 1.14
go build -mod=mod # явно проигнорировать vendor
go mod verify # проверить, что кэш не меняли после скачивания (с go.sum не сверяет)
go clean -modcache # снести кэш целиком: read-only он снимет сам, rm -rf упрётся в права
# Воспроизводимая сборка в CI: три флага, которые стоит знать
go build -mod=readonly ./... # запретить менять go.mod (в 1.16+ это дефолт)
go mod download # только скачать: go.mod не трогает, но может дописать go.sum
go mod tidy -diff # Go 1.23: показать, что бы изменилось, и упасть если есть diff
Современный дефолт: не вендорить. MVS плюс go.sum уже дают воспроизводимость, а прокси даёт доступность. Вендоринг оправдан в трёх случаях: сборка в закрытом контуре без своего прокси, регуляторное требование держать весь код зависимостей в репозитории, обязанность ревьюить любые изменения в сторонних библиотеках (финтех, безопасность). Если вендоришь, запускай go mod vendor в CI и роняй сборку при расхождении, иначе vendor/ тихо разъедется с go.mod.
Циклические импорты: почему запрещены и как разруливать
Go не допускает циклов в графе импортов вообще: ни прямых (a → b → a), ни транзитивных. Попытка даёт ошибку компиляции import cycle not allowed со всей цепочкой. Причин три, и назвать лучше все:
-
Скорость компиляции. Пакет компилируется один раз, результат ложится в
.a-архив и переиспользуется. Ациклический граф даёт топологическую сортировку: каждый пакет собирается после всех, от кого зависит, и параллельно с независимыми. С циклами группу пакетов пришлось бы компилировать совместно, и любая правка пересобирала бы всю группу. -
Детерминированная инициализация. Порядок
init()задаёт топологическая сортировка графа. В цикле «кто первый» не определить. - Дизайн. Цикл почти всегда означает, что границы пакетов проведены неправильно: либо два пакета на самом деле составляют один, либо между ними не хватает третьего.
// ── Было: цикл ──────────────────────────────────────────────
// package order
import "app/user"
func (s *Service) Create(uid int64) error {
u, err := user.Get(uid) // order → user
...
}
// package user
import "app/order"
func Get(id int64) (User, error) {
last := order.LastFor(id) // user → order ← цикл
...
}
// ── Стало: интерфейс у потребителя ──────────────────────────
// package order объявляет то, что ему нужно, и ничего не импортирует
type UserGetter interface {
Get(ctx context.Context, id int64) (User, error)
}
type Service struct {
users UserGetter
}
func New(users UserGetter) *Service { return &Service{users: users} }
func (s *Service) Create(ctx context.Context, uid int64) error {
u, err := s.users.Get(ctx, uid) // никакого import "app/user"
...
}
// package user просто имеет подходящий метод, про интерфейс не знает
func (s *Store) Get(ctx context.Context, id int64) (order.User, error) { ... }
// package main - единственное место, где встречаются оба
svc := order.New(user.NewStore(db))
Тестовый файл foo_test.go с package foo живёт внутри пакета и участвует в графе импортов: из него нельзя импортировать пакет, который импортирует foo. А файл с package foo_test (external test package) компилируется отдельным пакетом и может импортировать что угодно, включая зависимых от foo. Так штатно проверяют связку двух пакетов, не создавая цикл, и заодно получают ответ на вопрос «зачем вообще нужен package x_test».
internal/: единственный механизм видимости на уровне
пакетов
Правило одно, и его проверяет тулчейн при сборке: пакет из директории internal/ можно импортировать только из кода, чей путь начинается с родителя этой internal/. Всё остальное получает ошибку use of internal package ... not allowed.
internal/. Единственный способ в Go сказать
«это деталь реализации, трогать снаружи нельзя», и единственный,
который проверяет сборка, а не код-ревью.
internal/
По умолчанию всё. Стандартная раскладка сервиса: /cmd/<binary> с тремя строками main, весь остальной код в /internal/..., а /pkg/... только для того, что ты намеренно публикуешь как API для чужих сервисов. Отсюда свобода рефакторинга: пока пакет в internal, меняй его сигнатуры сколько угодно, мажорную версию выпускать не придётся. В стандартной библиотеке internal работает по тому же правилу, например net/http/internal.
init() и полный порядок инициализации
Полный порядок инициализации укладывается в пять шагов.
go build, и полагаться на неё нельзя.
// ── Полные правила ──────────────────────────────────────────
// 1. Пакет инициализируется ровно один раз, даже если импортирован из десяти мест.
// 2. Сначала все импортируемые пакеты, рекурсивно, в порядке топологической сортировки.
// 3. Внутри пакета: переменные уровня пакета в порядке зависимостей между ними.
// 4. Затем все init() этого пакета. Их может быть несколько в одном файле.
// 5. init() нельзя вызвать вручную, нельзя взять его адрес, нет параметров и результата.
// 6. Только после инициализации всех пакетов запускается main.main().
package config
var (
Timeout = defaultTimeout * 2 // зависит от defaultTimeout → будет вторым
defaultTimeout = 15 * time.Second // будет первым
)
func init() { fmt.Println("init 1") }
func init() { fmt.Println("init 2") } // обе выполнятся, в порядке появления в файле
// Импорт ради побочного эффекта - главное хорошее применение init:
import (
_ "github.com/lib/pq" // регистрирует драйвер в database/sql
_ "net/http/pprof" // вешает хендлеры на DefaultServeMux
_ "go.uber.org/automaxprocs" // правил GOMAXPROCS под cgroup;
// с Go 1.25 рантайм умеет это сам
)
init(): развёрнутый ответ
-
Невозможно вернуть ошибку. На сбой остаётся ответить только
panicилиlog.Fatal: процесс падает на старте без внятного контекста. -
Невозможно передать параметры. Конфиг берётся из глобальных переменных или прямо из
os.Getenv, и зависимость прячется. -
Ломает тесты.
initвыполняется при любом запуске тестов пакета, даже если тестируется не он. Пакет, который вinitлезет в базу или читает файл, делает свои тесты нелокальными. -
Порядок неочевиден. Читая файл, ты не видишь, что раньше отработали три
initиз импортов. Отладка «почему переменная уже не nil» превращается в квест. -
Транзитивные побочные эффекты. Классика: кто-то в глубине зависимостей импортировал
net/http/pprof, и твой публичный сервер на дефолтном мультиплексоре внезапно отдаёт/debug/pprof/в интернет. -
Замедляет старт. Всё, что в
init, выполняется доmainи до того, как заработает readiness-проба.
// ── Как надо вместо init ────────────────────────────────────
// Плохо: скрытая зависимость, паника на старте, нетестируемо
var db *sql.DB
func init() {
var err error
db, err = sql.Open("postgres", os.Getenv("DSN"))
if err != nil { log.Fatal(err) } // никакого контекста, defer-ы не отработают
}
// Хорошо: явный конструктор с ошибкой, вызывается из main
func NewDB(ctx context.Context, dsn string) (*sql.DB, error) {
db, err := sql.Open("postgres", dsn)
if err != nil { return nil, fmt.Errorf("open db: %w", err) }
if err := db.PingContext(ctx); err != nil { return nil, fmt.Errorf("ping db: %w", err) }
return db, nil
}
// Ленивая инициализация вместо init, если объект дорогой
var (
once sync.Once
client *http.Client
)
func Client() *http.Client {
once.Do(func() { client = buildClient() })
return client
}
// Go 1.21: sync.OnceValue / sync.OnceValues - то же самое, но короче
var Client = sync.OnceValue(buildClient)
init всё-таки уместен
Три случая: (1) регистрация в реестре, то есть драйверы database/sql, кодеки image, форматы encoding; (2) константы, которые нельзя выразить литералом и приходится собирать на старте: regexp.MustCompile, template.Must; (3) проверка инвариантов сборки, например что таблица переходов заполнена. Общее у них одно: работа детерминированная, локальная и без внешнего мира, то есть ни сети, ни файлов, ни базы, ни переменных окружения.
Инструментарий
| Инструмент | Что делает | Где живёт | Ловит примеры |
|---|---|---|---|
gofmt |
Канонический формат кода. Не настраивается - в этом весь смысл | В тулчейне | Споры о стиле в ревью |
goimports |
gofmt + автоматические импорты и их
группировка
|
x/tools |
Забытые/лишние импорты |
go vet |
Набор анализаторов на «подозрительное, но компилируемое» |
В тулчейне, часть проверок запускается сама при go test
|
Printf-формат, копирование мьютекса,
недостижимый код, потерянный cancel
|
staticcheck |
Самый мощный отдельный анализатор: ~150 проверок SA/S/ST/QF | honnef.co/go/tools |
Мёртвый код, неверные сравнения, неэффективные конструкции, деприкейты |
golangci-lint |
Раннер десятков линтеров с кэшем и параллелизмом. Сам ничего не анализирует | Отдельный бинарь | Всё вышеперечисленное разом + errcheck, revive, gosec, bodyclose… |
go generate |
Выполняет команды из комментариев
//go:generate. Не запускается сам при build
|
В тулчейне | mockgen, stringer, protoc, sqlc, ent |
go test -race |
Детектор гонок (ThreadSanitizer) | В тулчейне | Гонки данных - но только на исполненном пути |
govulncheck |
CVE в зависимостях, с проверкой достижимости уязвимой функции | x/vuln |
Уязвимости, которые реально вызываются вашим кодом |
pprof / trace |
Профили CPU/heap/block/mutex и трасса планировщика | В тулчейне | Горячие места, аллокации, блокировки |
# Минимальный набор в CI, который стоит уметь назвать
gofmt -l . # список неотформатированных файлов; пусто = ок
go vet ./...
go build ./...
go test -race -count=1 ./...
go mod tidy -diff # Go 1.23: упасть, если go.mod/go.sum не в порядке
golangci-lint run --timeout=5m
govulncheck ./...
# .golangci.yml - рабочий минимум без фанатизма (формат golangci-lint v2)
version: "2"
linters:
enable:
- errcheck # непроверенные ошибки
- govet
- staticcheck
- revive # замена golint: стиль и именование
- ineffassign # присваивания, которые никто не читает
- bodyclose # незакрытый resp.Body
- rowserrcheck # забытый rows.Err()
- sqlclosecheck
- errorlint # неправильные сравнения ошибок вместо errors.Is
- noctx # HTTP-запрос без context
exclusions:
rules:
- path: _test\.go
linters: [errcheck, gosec]
// go:generate - код рядом с тем, что он генерирует
//go:generate mockgen -source=$GOFILE -destination=mocks/store_mock.go -package=mocks
//go:generate stringer -type=Status -linecomment
type Status int
const (
StatusNew Status = iota // new
StatusPaid // paid
StatusShipped // shipped
)
// stringer сгенерирует Status.String() без рефлексии и без ручного switch.
// Go 1.24: инструменты фиксируются в go.mod, а не в файле tools.go с build-тегом
// go get -tool golang.org/x/tools/cmd/stringer
// go tool stringer -type=Status
// Раньше приходилось держать такое:
// //go:build tools
// package tools
// import _ "golang.org/x/tools/cmd/stringer"
«go vet встроенный, консервативный, почти без ложных срабатываний; часть его проверок запускается автоматически при go test, а остальные гоняю в CI, и его находки чиню всегда. staticcheck отдельный и гораздо более глубокий анализатор. А golangci-lint вообще не линтер, а раннер: гоняет и vet, и staticcheck, и ещё три десятка, переиспользуя один разбор AST и кэшируя результат, поэтому в CI это на порядок быстрее, чем запускать их по одному.»
Билд-теги, кросс-компиляция и CGO
//go:build linux && amd64 && !nocgo
// Синтаксис с Go 1.17. Старая форма // +build до сих пор встречается в чужом коде.
// Строка должна идти до package. Пустую строку после неё ставит gofmt, а для // +build она обязательна.
package storage
// Выражение поддерживает && || ! и скобки. Доступные теги: GOOS, GOARCH,
// имя компилятора (gc, gccgo), cgo, версия тулчейна (go1.21 и выше), и свои через -tags.
# Неявные теги по имени файла работают без строки //go:build
store_linux.go # GOOS=linux (и android)
store_windows_amd64.go # GOOS=windows и GOARCH=amd64
store_test.go # только при go test
export_test.go # обычный тестовый файл, приём «открыть приватное для тестов»
# Свои теги
go build -tags="integration,e2e" ./...
go test -tags=integration ./...
# Кросс-компиляция: обычно одна строка, без тулчейнов и sysroot
GOOS=linux GOARCH=amd64 go build -o app .
GOOS=darwin GOARCH=arm64 go build -o app-mac .
GOOS=windows GOARCH=amd64 go build -o app.exe .
go tool dist list # полный список поддерживаемых пар
# Прод-сборка контейнерного бинаря
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -trimpath -ldflags="-s -w -X main.version=$(git describe --tags)" -o /app .
# CGO_ENABLED=0 → полностью статический бинарь, работает в scratch/distroless
# -trimpath → убирает абсолютные пути из бинаря (воспроизводимость + не течёт /home/user)
# -s -w → выкинуть таблицу символов и DWARF: минус ~30% размера
# -X → вшить версию в переменную на этапе линковки
| Что даёт CGO | Чего стоит |
|---|---|
| Доступ к любым C-библиотекам (SQLite, libpq, OpenCV, ffmpeg) | Нужен C-компилятор → кросс-компиляция ломается, требуется тулчейн под каждую цель |
| Системные API, которых нет в стандартной библиотеке |
Бинарь становится динамически слинкованным:
scratch-образ больше не работает
|
Полная реализация os/user и DNS через
системный резолвер
|
Каждый вызов C - переход в состояние syscall (затянется вызов - sysmon отберёт у горутины P) и переключение на системный стек. Оверхед порядка десятков наносекунд |
| Постепенная миграция legacy C-кода | C-код не под наблюдением GC: утечки, отсутствие panic/recover, падения ломают весь процесс |
| - |
Ломается часть инструментов: -race не видит
гонки внутри C, профилировщик хуже показывает C-стеки
|
| - | Растёт время сборки, усложняется Dockerfile, появляется зависимость от glibc/musl |
CGO_ENABLED=0 для сервисов идёт дефолтом. Включай только под конкретную нужду и лучше выноси её в отдельный бинарь, а не тащи в основной. Самый частый случай: mattn/go-sqlite3 требует CGO, хотя есть чистая замена modernc.org/sqlite. Второй случай, DNS: с CGO_ENABLED=0 работает чистый Go-резолвер, который игнорирует часть настроек nsswitch.conf. В Kubernetes это обычно не проблема, в гибридных сетях иногда всплывает.
Что нового в свежих версиях Go
Этим вопросом проверяют одно: следишь ли ты за языком вообще. Достаточно назвать по 2–3 значимых пункта на релиз и объяснить, зачем их сделали. Релизы выходят строго дважды в год, в феврале и августе: 1.24 в феврале 2025, 1.25 в августе 2025, 1.26 в феврале 2026, 1.27 в августе 2026, текущая.
| Версия | Главное | Зачем |
|---|---|---|
|
1.22 фев 2024 |
Новая семантика переменной цикла: своя копия на каждую итерацию |
Убрана самая известная ловушка языка. Включается строкой
go 1.22 в go.mod - по модулю, а не по
тулчейну
|
Паттерны в http.ServeMux: методы и wildcard-сегменты
|
Роутер в stdlib наконец покрывает 80% случаев без chi/gorilla | |
for i := range 10 - range по целому |
Убирает шум for i := 0; i < 10; i++ |
|
math/rand/v2, slices.Concat,
PGO-сборки быстрее на 2–14%
|
Первый /v2 в самой стандартной библиотеке
|
|
|
1.23 авг 2024 |
Итераторы: range over func + пакет
iter
|
Единый протокол обхода для любых коллекций - до этого каждый писал свой |
Итераторные функции в slices/maps
|
slices.Sorted(maps.Keys(m)) вместо шести
строк
|
|
Пакет unique; таймеры стали GC-безопасными,
канал у Timer теперь без буфера
|
Интернирование строк; убрана классическая утечка
неостановленного time.After в
select
|
|
|
1.24 фев 2025 |
Swiss Tables - новая реализация мап | Поиск сразу по 8 контрольным байтам (на amd64 через SIMD): до 60% быстрее на микробенчмарках, меньше памяти. Прозрачно для кода |
Generic type aliases:
type Set[T comparable] = map[T]struct{}
|
Закрыт пробел дженериков: алиасы наконец могут иметь параметры типа | |
tool-директива в go.mod +
go tool
|
Хак с tools.go и build-тегом больше не нужен
|
|
b.Loop(), os.Root, weak,
runtime.AddCleanup
|
for b.Loop() чинит целый класс ошибок в
бенчмарках; os.Root - защита от path
traversal
|
|
|
1.25 авг 2025 |
WaitGroup.Go
|
Один метод вместо связки Add(1) +
go + defer Done() - убирает
забытый Done и рассинхрон счётчика
|
Container-aware GOMAXPROCS
|
Рантайм читает CPU-лимит cgroup и подстраивается на лету.
automaxprocs больше не нужен
|
|
testing/synctest - стабильный |
Детерминированные тесты конкурентного кода с «виртуальным» временем | |
GC Green Tea и encoding/json/v2 -
тогда ещё эксперименты, flight recorder в
runtime/trace
|
Оба эксперимента с тех пор доехали до дефолта: Green Tea - в 1.26, json/v2 - в 1.27 | |
|
1.26 фев 2026 |
Green Tea GC включён по умолчанию |
Минус 10–40% накладных расходов сборщика, плюс ещё около
10% на свежих amd64 за счёт векторных инструкций.
Выключить: GOEXPERIMENT=nogreenteagc
|
Два изменения в языке: new принимает
выражение (p := new(compute())) и
дженерик-тип может ссылаться на себя в констрейнте (type Adder[A Adder[A]] interface{ Add(A) A })
|
Первое убивает самописные ptr[T]-помощники,
второе позволяет выразить «тип, который складывается сам с
собой» без any
|
|
errors.AsType[T](err),
slog.NewMultiHandler,
io.ReadAll вдвое быстрее
|
AsType - дженерик-версия
errors.As: типобезопасно и без объявления
переменной заранее
|
|
Экспериментальный профиль goroutineleak; вызовы cgo дешевле на ~30%; редирект
ServeMux на слэш стал 307 вместо 301;
go fix переписан в набор «модернизаторов»
|
Профиль находит горутины, навсегда заблокированные на
недостижимых примитивах - до этого утечки ловили только
NumGoroutine и goleak
|
|
|
1.27 авг 2026 |
У методов появились собственные параметры типа |
Снято главное ограничение дженериков. Запрет остался на
стыке с интерфейсами: у метода интерфейса параметров
типа нет, а дженерик-метод не засчитывается в реализацию. В
stdlib пример - Rand.N() в
math/rand/v2
|
encoding/json/v2 как отдельный пакет
|
Строже старого: отвергает невалидный UTF-8 и дублирующиеся
ключи объекта, Unmarshal заметно быстрее. Сам
encoding/json теперь работает поверх v2, но ведёт себя по-старому (могут отличаться тексты ошибок) - строгость
включает только импорт v2. Рядом появился
низкоуровневый encoding/json/jsontext. Вернуть прежнюю реализацию:
GOEXPERIMENT=nojsonv2, пакеты v2 при этом пропадут
|
|
Пакет uuid в стандартной библиотеке
(RFC 9562)
|
NewV4(), NewV7(),
Parse. NewV7 сортируется по
времени - то, что нужно для первичных ключей. Внешний
github.com/google/uuid больше не обязателен
|
|
Профиль goroutineleak стал
общедоступным; strings.CutLast; часть мелких
аллокаций дешевле до 30%; метки горутин попадают в
трейсбеки; удалён GODEBUG=asynctimerchan
|
Утечки горутин теперь ищутся штатным профилем -
/debug/pprof/goroutineleak. Метки в трейсбеке
сразу показывают, какой запрос обрабатывала упавшая
горутина
|
// ── 1.22: ServeMux ──────────────────────────────────────────
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id") // новый метод для wildcard-сегментов
...
})
mux.HandleFunc("POST /users", createUser)
mux.HandleFunc("GET /files/{path...}", serveFiles) // многосегментный wildcard
// Выигрывает более «специфичный» паттерн, порядок регистрации не важен.
// Конфликтующие паттерны вызывают панику при регистрации, а не тихо перекрывают друг друга.
// ── 1.23: итераторы ─────────────────────────────────────────
// Три сигнатуры, которые понимает range:
// func(yield func() bool)
// func(yield func(V) bool) → iter.Seq[V]
// func(yield func(K, V) bool) → iter.Seq2[K, V]
func Lines(r io.Reader) iter.Seq2[string, error] {
return func(yield func(string, error) bool) {
sc := bufio.NewScanner(r)
for sc.Scan() {
if !yield(sc.Text(), nil) { return } // false = потребитель сделал break
}
if err := sc.Err(); err != nil { yield("", err) }
}
}
for line, err := range Lines(f) { // выглядит как обычный range
if err != nil { return err }
process(line)
}
// ── 1.24: b.Loop ────────────────────────────────────────────
func BenchmarkParse(b *testing.B) {
data := load()
for b.Loop() { // вместо for i := 0; i < b.N; i++
_ = Parse(data) // аргументы и результат не «оптимизируются» компилятором,
} // а setup вне цикла не попадает в замер
}
// ── 1.25: WaitGroup.Go ──────────────────────────────────────
var wg sync.WaitGroup
for _, u := range urls {
wg.Go(func() { fetch(u) }) // Add(1) + go + defer Done() внутри
}
wg.Wait()
// ── 1.26: new от выражения и errors.AsType ──────────────────
type Req struct {
Limit *int
}
r := Req{Limit: new(50)} // раньше: tmp := 50; r := Req{Limit: &tmp}
// errors.AsType - дженерик-версия errors.As: без объявления переменной заранее
if pe, ok := errors.AsType[*fs.PathError](err); ok {
log.Println(pe.Path)
}
// старая форма: var pe *fs.PathError; if errors.As(err, &pe) { ... }
// 1.26: дженерик-тип ссылается на себя в собственном констрейнте
type Adder[A Adder[A]] interface{ Add(A) A }
// ── 1.27: дженерик-метод и uuid в stdlib ────────────────────
type Box struct {
v []int
}
func (b *Box) MapTo[T any](f func(int) T) []T { // до 1.27 не компилировалось
out := make([]T, 0, len(b.v))
for _, x := range b.v { out = append(out, f(x)) }
return out
}
id := uuid.NewV7() // "uuid" теперь в стандартной библиотеке
fmt.Println(id.String()) // сортируемый по времени → годится в PK
// 1.27: поиск утечек горутин штатным профилем
// go tool pprof http://localhost:6060/debug/pprof/goroutineleak
range над функцией работает как синтаксический сахар: тело цикла становится колбэком yield, а break превращается в возврат false. Главное тут: циклом управляет итератор, а не потребитель.
Go 1.24 заменил классическую реализацию мапы (массив бакетов по 8 пар + цепочка overflow) на Swiss Table: группы по 8 слотов с отдельным control word из 8 байт, где каждый байт хранит 7 бит хеша плюс флаг занятости. Поиск сравнивает сразу все 8 контрольных байтов: на amd64 одной SIMD-инструкцией, на остальных архитектурах несколькими битовыми операциями над 64-битным словом. Поэтому промах определяется за один шаг, а не за обход цепочки. На микробенчмарках это до 60% ускорения, по реальным приложениям в среднем около 1,5% CPU, плюс меньше накладной памяти. Для кода не меняется ничего: порядок обхода по-прежнему случайный, мапа по-прежнему не потокобезопасна, адрес значения по-прежнему нельзя взять.
Вопросы
13go.mod вниз). Модуль - это набор пакетов,
которые выпускаются вместе под одной версией.
| Пакет | Модуль | |
|---|---|---|
| Граница | Директория (без вложенных) | go.mod и всё дерево ниже |
| Объявление | package name |
module github.com/org/repo |
| Версия | Нет | Semver-тег |
| Что импортируют | Пакет, по полному пути | Модуль не импортируют |
| Видимость | Заглавная буква идентификатора | internal/ |
github.com/acme/billing/ ← модуль (тут go.mod)
├── go.mod go.sum
├── main.go ← пакет main
├── invoice/ ← пакет invoice
│ ├── invoice.go
│ ├── total.go ← тот же пакет: файлы в одной папке
│ └── pdf/ ← ДРУГОЙ пакет invoice/pdf
└── internal/store/ ← пакет store, виден только внутри модуля
# Путь импорта = путь модуля + относительный путь директории:
import "github.com/acme/billing/invoice/pdf"
Несколько модулей в репозитории
Технически можно: положи go.mod в подпапку, и она станет отдельным модулем (её содержимое автоматически исключается из родительского). Но цена высокая: теги превращаются в subdir/v1.2.0, версии расходятся, релиз усложняется. На практике так делают ради двух вещей: (1) вынести пакет с тяжёлыми зависимостями (например, gRPC-контракты), чтобы потребители не тянули лишнего; (2) отделить примеры или e2e-тесты. В остальных случаях: один репозиторий, один модуль.
Имя пакета в коде и последний сегмент пути импорта совпадать не обязаны. import "gopkg.in/yaml.v3" даёт идентификатор yaml, а не yaml.v3; github.com/mattn/go-sqlite3 даёт пакет sqlite3. Отсюда алиасы импорта и отсюда же промахи goimports, который иногда «угадывает» неверно.
go.mod хранит путь модуля,
языковую версию, список минимально требуемых версий
зависимостей и локальные корректировки графа. Директива
go - это языковая версия, а с Go 1.21 ещё и
минимальная версия тулчейна: от неё зависит доступная
семантика.
module github.com/acme/billing
go 1.24.0 // языковая версия для всех пакетов модуля
toolchain go1.24.3 // какой тулчейн скачать, если локальный младше
require (
github.com/jackc/pgx/v5 v5.6.0
go.uber.org/zap v1.27.0
)
require github.com/pkg/errors v0.9.1 // indirect
replace github.com/broken/lib => ../fork // подмена источника или версии
exclude github.com/bad/lib v1.2.3 // «эту версию не рассматривать»
retract [v1.0.0, v1.0.4] // отозвать свои выпущенные версии
tool golang.org/x/tools/cmd/stringer // Go 1.24: инструменты как зависимости
Директива go - три факта
-
Задаёт, какие языковые фичи разрешены и по какой семантике работает код. Пример:
go 1.22включает новую семантику переменной цикла, а модуль сgo 1.21в том же билде продолжает работать по-старому. Семантику определяет модуль, а не версия тулчейна. -
С Go 1.21 она жёсткая: тулчейн младше указанного
откажется собирать, а не выдаст предупреждение. При
GOTOOLCHAIN=autoнужная версия скачается сама. -
Собрать модуль с
go 1.19новым компилятором можно и нужно: обратная совместимость соблюдается.
replace vs exclude vs
retract
| Директива | Что делает | Кто видит | Типичное применение |
|---|---|---|---|
replace |
Подменяет модуль другим путём/версией | Только главный модуль | Локальный форк, отладка, монорепо |
exclude |
Убирает версию из рассмотрения MVS | Только главный модуль | Битый релиз с CVE |
retract |
Помечает собственную версию отозванной | Все потребители | Ошибочно выпущенный тег |
replace
В библиотеке replace не работает: он применяется, только когда модуль главный. Классический инцидент: разработчик добавил replace для локальной отладки, забыл убрать, выпустил тег. У него всё собирается, а потребителям приезжает оригинальная (сломанная) версия. Для локальной разработки бери workspaces: go work init . ../lib, файл go.work не коммитят, go.mod при этом вообще не трогают.
go.sum - не lock-файл.
Версии фиксирует go.mod + MVS;
go.sum отвечает за целостность: хранит
хеши контента каждой версии. Checksum database
(sum.golang.org) - глобальный append-only лог,
который гарантирует, что все в мире видят один и тот же
контент.
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexB... # хеш дерева файлов модуля
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlr... # хеш только его go.mod
Почему две строки
Чтобы посчитать граф зависимостей, MVS нужны только go.mod-файлы всех участников, сам код скачивать не обязательно. Поэтому хеш go.mod хранится отдельно. Отсюда же модули в go.sum, чей код в сборку не попал: их go.mod участвовал в вычислении версий. Чистить руками не надо, для этого есть go mod tidy.
Чем это отличается от package-lock.json
| go.sum | package-lock.json | |
|---|---|---|
| Фиксирует версии | Нет - это делает go.mod + MVS | Да, это его основная роль |
| Фиксирует хеши | Да | Да |
| Если файла нет | go build падает: missing go.sum entry |
Установка идёт, но версии плывут |
| Конфликты при мерже | Редко и легко: go mod tidy |
Регулярно и болезненно |
| Глобальная проверка | Да - sumdb | Нет |
Checksum database
sum.golang.org ведёт прозрачный лог на дереве Меркла, только на добавление, по устройству похожий на Certificate Transparency. Он закрывает дыру, которую go.sum не закрывает: go.sum защищает тебя после того, как ты один раз зафиксировал хеш, а sumdb защищает первое скачивание, чтобы никто не подсунул тебе одну версию контента, а всему миру другую. Побочный, но важный эффект: теги становятся неизменяемыми де-факто.
# Что делать при SECURITY ERROR: checksum mismatch
# 1. Не делать go clean -modcache и не стирать go.sum «чтобы заработало»
# 2. Понять, чей хеш не сошёлся:
go mod download -x github.com/x/y
curl -s https://sum.golang.org/lookup/github.com/x/y@v1.2.3 # хеш, который видят все
# 3. Три реальные причины по убыванию частоты:
# а) автор переписал тег (git tag -f + push --force), а модуль берут мимо прокси: писать автору, пинить другую версию
# б) в корпоративном прокси лежит подменённый/битый артефакт: чистить прокси
# в) go.sum поехал при мерже: восстановить и пересобрать (go mod tidy)
# Приватные модули настраиваются одной переменной:
go env -w GOPRIVATE=github.com/acme # выключает и прокси, и sumdb для этих путей
git config --global url."git@github.com:".insteadOf "https://github.com/"
«go.sum не про версии, а про то, что скачанный контент тот же, что был при первой сборке. Версии определяют go.mod и MVS, и они детерминированы сами по себе, поэтому Go обходится без lock-файла. И go.sum обязательно коммитим: без него теряется вся защита цепочки поставки.»
/v2, /v3. Побочный и очень
полезный эффект: две мажорные версии могут сосуществовать в
одной программе.
// v0 и v1 - без суффикса
import "github.com/acme/lib" // v0.x.x или v1.x.x
// v2+ - суффикс обязателен и в go.mod, и в импорте
import "github.com/acme/lib/v2" // module github.com/acme/lib/v2
// Побочный эффект: обе версии живут рядом, это разные модули
import (
pgx4 "github.com/jackc/pgx/v4"
pgx5 "github.com/jackc/pgx/v5"
)
// Так большой сервис переводят по пакету за раз, а не одним коммитом.
Три формы версии, которые надо узнавать
| Форма | Пример | Что означает |
|---|---|---|
| Обычный тег | v1.5.2 |
Semver-тег в репозитории |
| Псевдоверсия |
v0.0.0-20240115143022-a1b2c3d4e5f6
|
На коммите нет тега: базовая версия + UTC-время коммита
+ 12 символов хеша. Если тег есть выше по истории, базой
берётся следующий патч: коммит после v1.5.2
даёт v1.5.3-0.…, что выше
v1.5.2
|
+incompatible |
v2.0.0+incompatible |
У модуля тег v2+, но нет
go.mod - доисторический пакет, Go
делает исключение
|
| Пререлиз | v1.5.0-rc.1 |
Ниже v1.5.0; go get -u с релиза на него
не перейдёт, а с псевдоверсии или пререлиза пониже - может
|
Как выпускают v2
# Вариант 1 - подпапка (major subdirectory)
mkdir v2 && cp *.go v2/ && cd v2 # v1 остаётся в корне для старых импортов
go mod init github.com/acme/lib/v2 # плюс переписать все внутренние импорты на /v2
git add . && git commit -m "v2" && git tag v2.0.0 && git push origin main v2.0.0
# Вариант 2 - ветка (major branch)
git checkout -b v2
# в go.mod: module github.com/acme/lib/v2, внутренние импорты тоже на /v2
git commit -am "v2" && git tag v2.0.0 && git push origin v2 v2.0.0
-
Тег
v2.0.0есть, а/v2вmoduleнет →go getругается «module path must match major version» и версия просто не устанавливается. -
Путь модуля поменяли, а внутренние импорты забыли → в
бинарь попадут обе копии пакета: два набора
глобальных переменных, два
init(), две регистрации в реестрах, значения одного типа не присваиваются другому. -
Считать, что
v0.xзащищён semver. Не защищён: в нулевой мажорке ломать разрешено в любом минорном релизе, и это не нарушение правил.
Алгоритм в трёх шагах
-
Обойти граф:
go.modглавного модуля →go.modего зависимостей → и так далее (нужны толькоgo.mod-файлы, код не скачивается). - Для каждого пути модуля собрать множество требуемых версий.
- Выбрать из множества максимальную. На выходе build list.
# main → A v1.2.0 → C v1.1.0
# main → B v1.5.0 → C v1.3.0
# В репозитории уже есть C v1.9.0
#
# Go: требования на C = {v1.1.0, v1.3.0} → берём v1.3.0
# npm: диапазоны ^1.1.0 и ^1.3.0 → берём самое свежее в пересечении, v1.9.0
go list -m all # итоговый build list
go mod graph # весь граф требований
go mod why -m github.com/x/y # зачем модуль в сборке: цепочка импортов
go list -m -u all # какие обновления доступны
| Go (MVS) | npm / Cargo / pip | |
|---|---|---|
| Форма требования | Точная минимальная версия |
Диапазон (^1.2, ~1.2)
|
| Что выбирается | Максимум из минимумов | Максимум, попадающий в диапазоны |
| Алгоритм | Обход графа, линейный | Решатель ограничений (в пределе NP-полный) |
| Lock-файл | Не нужен | Обязателен |
| Сборка вчера и через год | Идентична | Может отличаться без lock-файла |
| Дубли версий | Одна версия на major | Дерево копий в node_modules |
| Патчи безопасности | Только явно | Часто приезжают сами |
Плюсы и минусы - оба надо назвать
-
+ High-fidelity builds. Набор версий определяет только содержимое
go.modв графе, ни дата сборки, ни состояние реестра на него не влияют. - + Нет «фантомных» апдейтов. Новая версия зависимости не приедет, пока кто-то явно не поднимет требование. Атака «выпустить вредоносный патч и ждать, пока CI сам подтянет» в Go не работает.
- + Простота. Нет NP-полного солвера, нет «resolution hell», нет протухшего лока.
-
− Обновления только вручную. Патчи безопасности сами не приезжают: нужен
go get -u, Renovate/Dependabot иgovulncheckв CI. -
− Ответственность распределена. Если популярная
библиотека требует старую версию с CVE, поднимать придётся
в каждом потребителе через
go getилиexclude.
Название сбивает с толку: берётся не самая маленькая версия из всех существующих, а самая большая из тех, что кто-то потребовал. «Минимальность» здесь значит «не берём больше, чем нужно»: v1.9.0 из репозитория не приедет, пока её никто не запросил. Ещё важно, что MVS работает по major-версиям раздельно, ведь lib и lib/v2 задают разные пути модуля, а значит и разные модули.
vendor/) и
позволяет собирать вообще без сети. Современный дефолт -
не вендорить: MVS + go.sum уже дают
воспроизводимость, а прокси - доступность.
Цепочка загрузки
-
Локальный кэш
$GOMODCACHE(по умолчанию$GOPATH/pkg/mod). Распакованные модули read-only плюс подкаталогcache/download, зеркало прокси в том же формате. -
Прокси из
GOPROXYзадаётся списком через запятую, по умолчаниюhttps://proxy.golang.org,direct. Прокси иммутабелен: раз опубликованную версию не изменить, а удаление тега в репозитории её из прокси обычно не убирает. -
directотправляет прямо в VCS (git clone). Срабатывает, если модуль не нашёлся в прокси или путь попал вGONOPROXY.
go mod vendor # ./vendor + vendor/modules.txt
# go build сам включает -mod=vendor, если vendor/ существует и go >= 1.14
go build -mod=mod # явно игнорировать vendor
go mod verify # проверить, что кэш не меняли после скачивания (с go.sum не сверяет)
go mod download # только скачать
go clean -modcache # снести кэш целиком
# Воспроизводимая сборка в CI
go build -mod=readonly ./... # запретить неявную правку go.mod (дефолт с 1.16)
go mod tidy -diff # Go 1.23: упасть, если tidy что-то изменил бы
| Подход | Когда оправдан | Цена |
|---|---|---|
| Публичный прокси | Дефолт для большинства | CI зависит от внешней сети |
| Свой прокси (Athens/Nexus/JFrog) | Закрытый контур, аудит, скорость | Надо поддерживать инфраструктуру |
| Вендоринг | Сборка без сети; регуляторное требование; ревью изменений в зависимостях | Репозиторий пухнет, конфликты при мерже, легко забыть пересобрать |
Кэш GOMODCACHE в CI |
Простое ускорение сборок | Кэш может протухнуть |
Разъезд версий ловится сам: если кто-то сделал go get и не запустил go mod vendor, сборка упадёт с «inconsistent vendoring». А вот vendor/ тихо разъезжается с go.mod, когда в vendor/ правят файлы руками: сборка молча берёт изменённый код, и go mod verify этого не заметит, он проверяет только кэш. Поэтому в CI обязательна проверка go mod vendor && git diff --exit-code
vendor/. Сам go сверяет с go.mod только vendor/modules.txt, а не код рядом с ним.
import cycle not allowed. Причины:
скорость компиляции (пакет собирается один раз, граф
топологически сортируется), детерминированный порядок
init() и качество дизайна. Разруливается тремя
приёмами, из которых главный -
интерфейс у потребителя.
Три причины запрета
- Компиляция. Каждый пакет компилируется ровно один раз, результат ложится в архив и переиспользуется. Ациклический граф даёт топологический порядок и параллельную сборку независимых веток. Циклы потребовали бы компилировать группу пакетов совместно, и любая правка пересобирала бы всю группу.
-
Инициализация. Порядок
init()задаёт топологическая сортировка. В цикле «кто первый» не определено. - Дизайн. Цикл почти всегда значит, что граница между пакетами проведена неправильно.
Четыре способа разорвать
| Приём | Когда подходит |
|---|---|
| Интерфейс у потребителя - объявить нужный контракт в том пакете, который его использует | Дефолт. Идиоматичный Go, попутно даёт тестируемость |
| Выделить общий пакет - вынести типы, нужные обоим, вниз по графу | Когда цикл из-за общих доменных типов |
Колбэк / DI - передать функцию, связывание в
main
|
Одна-две точки взаимодействия, интерфейс избыточен |
| Объединить пакеты | Когда они и правда об одном и том же и разделены искусственно |
// package order объявляет то, что ему нужно, и ничего не импортирует из user
type UserGetter interface {
Get(ctx context.Context, id int64) (User, error)
}
type Service struct {
users UserGetter
}
func New(users UserGetter) *Service { return &Service{users: users} }
func (s *Service) Create(ctx context.Context, uid int64) error {
u, err := s.users.Get(ctx, uid) // цикла нет: импорта user тут нет
...
}
// package user просто имеет подходящий метод, про интерфейс не знает
func (s *Store) Get(ctx context.Context, id int64) (order.User, error) { ... }
// package main - единственное место, где встречаются оба
svc := order.New(user.NewStore(db))
В Java интерфейс живёт рядом с реализацией, и потребитель импортирует пакет с интерфейсом. В Go реализация неявная: типу не нужно знать про интерфейс, чтобы ему удовлетворять. Поэтому интерфейс объявляют там, где его используют, и стрелка зависимости разворачивается: пакет-реализацию больше не импортируют вообще. Разом уходит цикл, интерфейс сужается до нужных методов, а в тестах его легко подменить. Короткая формулировка ходит в сообществе, в документации Go её нет: «Accept interfaces, return structs».
-
Схлопывать всё в общий пакет
models/на 200 типов: цикл уйдёт, модульность умрёт. -
Городить глобальный реестр с регистрацией через
init(): тот же цикл, только теперь во время выполнения и без проверки компилятором. -
Переходить на
anyи рефлексию, лишь бы не импортировать пакет.
foo_test.go с package foo входит в пакет foo, и запрет цикла на него распространяется. А файл с package foo_test (external test package) компилируется отдельным пакетом и может импортировать что угодно, включая тех, кто зависит от foo. Штатный способ проверить связку двух пакетов, не создавая цикл, и заодно ответ на вопрос «зачем вообще нужен package x_test».
.../X/internal/Y может
импортировать только код, чей путь начинается с
.../X - то есть из дерева, начинающегося с
родителя директории internal. Проверяет
тулчейн при сборке (команда go), а не соглашение и не линтер.
github.com/acme/app/
├── cmd/api/ → может импортировать internal/* (внутри acme/app)
├── pkg/client/ → может импортировать internal/* (внутри acme/app)
└── internal/
├── store/ ← родитель internal = github.com/acme/app
└── auth/
└── internal/jwt/ ← родитель = .../app/internal/auth
→ виден ТОЛЬКО из auth и его поддерева
# А так не скомпилируется:
# package github.com/other/svc
import "github.com/acme/app/internal/store"
# → use of internal package github.com/acme/app/internal/store not allowed
Что важно понимать
-
Уровень
internalможет быть любым и вложенным, поэтому внутри модуля строится настоящая иерархия видимости, а не один плоский «приватный» слой. -
Правило работает и в стандартной библиотеке, по тому же принципу:
net/http/internal,crypto/internal/.... -
Ограничение действует на уровне путей импорта, а не модулей. Если два модуля лежат в одном дереве путей,
internalмежду ними пропустит. Случай редкий, но в монорепо встречается. - Это единственный механизм видимости на уровне пакетов. Всё остальное сводится к заглавной или строчной букве идентификатора внутри одного пакета.
По умолчанию всё в internal/. Типичная структура: /cmd/<binary>/main.go на три строки, вся логика в /internal/..., а /pkg/... только для того, что ты намеренно публикуешь как API для чужих сервисов. Выгода: пока пакет в internal, его сигнатуры можно менять сколько угодно, не выпуская мажорную версию и не ломая чужой код. Сборка гарантирует, что снаружи на него никто не завязался.
«internal это как private». Не совсем: internal ограничивает импорт пакета, а не видимость идентификаторов. Внутри internal/store экспортированные имена так и остаются экспортированными, просто добраться до них может только код из того же дерева. И обратное: на рефлексию и линковку internal не влияет никак, это правило сборки для разрешения импортов.
init(), и только
потом main.main(). Каждый пакет
инициализируется ровно один раз.
Полный список правил
- Рантайм поднимается раньше всего: планировщик, аллокатор, GC, обработка сигналов.
- Импортируемые пакеты: рекурсивно и полностью, в порядке топологической сортировки графа импортов. Цикл невозможен, поэтому порядок всегда определён.
-
Переменные уровня пакета: в порядке зависимостей между ними. Порядок объявления в файле важен только между независимыми переменными: из готовых к инициализации берётся первая по объявлению. Цикл в зависимостях переменных даёт ошибку компиляции
initialization cycle. -
Все
init()пакета. Их может быть несколько в одном файле и несколько файлов. Порядок между файлами не гарантирован спецификацией, ноgo buildпередаёт файлы отсортированными по имени. -
init()нельзя вызвать вручную, нельзя взять его адрес, у него нет параметров и результата. -
Только после инициализации всех пакетов запускается
main.main().
package main
var a = b + 1 // третьей: зависит от b
var b = f() // второй: зависит от c через f()
func f() int { return c }
var c = 1 // первой: ни от чего не зависит
func init() { fmt.Println("init 1", a, b, c) }
func init() { fmt.Println("init 2") } // обе выполнятся, сверху вниз
func main() { fmt.Println("main", a) }
// вывод:
// init 1 2 1 1 (c = 1 → b = f() = 1 → a = b+1 = 2)
// init 2
// main 2
Чем плохи init()
-
Нельзя вернуть ошибку. На сбой остаётся только паника или
log.Fatal: падение на старте без контекста и без выполнения defer-ов. -
Нельзя передать параметры. Конфиг берётся из глобальных переменных или прямо из
os.Getenv, и зависимость становится скрытой. -
Ломают тесты.
initвыполняется при любом запуске тестов пакета. Пакет, который вinitходит в базу или читает файл, делает свои тесты нелокальными и нестабильными. -
Порядок неочевиден. Читая код, ты не видишь, что до него отработали три
initиз зависимостей. -
Транзитивные побочные эффекты. Классика: кто-то в глубине зависимостей импортировал
net/http/pprof, и твой публичный сервер на дефолтном мультиплексоре отдаёт/debug/pprof/наружу. -
Замедляют старт. Всё это происходит до
mainи до готовности readiness-пробы.
// Плохо
var db *sql.DB
func init() {
var err error
db, err = sql.Open("postgres", os.Getenv("DSN"))
if err != nil { log.Fatal(err) }
}
// Хорошо: явный конструктор, ошибка возвращается, вызывается из main
func NewDB(ctx context.Context, dsn string) (*sql.DB, error) {
db, err := sql.Open("postgres", dsn)
if err != nil { return nil, fmt.Errorf("open db: %w", err) }
if err := db.PingContext(ctx); err != nil { return nil, fmt.Errorf("ping db: %w", err) }
return db, nil
}
// Дорогой объект создают лениво через sync.Once, а не в init
var Client = sync.OnceValue(buildClient) // Go 1.21
init уместен
Три случая: регистрация в реестре (драйверы database/sql, кодеки image и encoding); «вычисляемые константы», которые нельзя выразить литералом (regexp.MustCompile, template.Must); проверка инвариантов сборки, например что таблица переходов заполнена целиком. Общее у них: работа детерминированная, локальная, без внешнего мира, то есть ни сети, ни файлов, ни базы, ни переменных окружения. И отдельно про import _ "...": единственная идиома, где init не костыль, а сам смысл импорта.
go vet - встроенный консервативный
анализатор (часть проверок запускается сама при go test);
staticcheck - внешний глубокий анализатор на
~150 проверок; golangci-lint - не линтер, а
раннер десятков линтеров с общим разбором AST и
кэшем.
| Инструмент | Роль | Примеры находок |
|---|---|---|
gofmt |
Канонический формат, не настраивается | Убирает споры о стиле из ревью как класс |
goimports |
gofmt + импорты: добавить, убрать,
сгруппировать
|
Забытые и лишние импорты |
go vet |
Встроенный, мало ложных срабатываний. Часть проверок
запускается автоматически при go test
|
Несовпадение Printf-формата и
аргументов, копирование структуры с мьютексом,
потерянный cancel, недостижимый код,
теги структур с опечаткой
|
staticcheck |
Внешний, самый глубокий одиночный анализатор | Мёртвый код, бесполезные присваивания, неверные сравнения, использование deprecated API, неэффективные конструкции |
golangci-lint |
Раннер: запускает vet, staticcheck и ещё десятки, переиспользуя один разбор | Всё вышеперечисленное + errcheck, revive, gosec, bodyclose, errorlint |
go generate |
Выполняет команды из //go:generate.
Не запускается при go build
|
mockgen, stringer, protoc, sqlc, ent |
govulncheck |
CVE с проверкой достижимости уязвимой функции из вашего кода | Отсеивает шум «уязвимость есть, но вы её не вызываете» |
# Минимальный набор в CI
gofmt -l . # пусто = ок
go vet ./...
go test -race -count=1 ./...
go mod tidy -diff # Go 1.23
golangci-lint run --timeout=5m
govulncheck ./...
//go:generate mockgen -source=$GOFILE -destination=mocks/store_mock.go -package=mocks
//go:generate stringer -type=Status -linecomment
type Status int
const (
StatusNew Status = iota // new
StatusPaid // paid
)
// Go 1.24: инструменты фиксируются прямо в go.mod
// go get -tool golang.org/x/tools/cmd/stringer
// go tool stringer -type=Status
// Раньше нужен был файл tools.go с build-тегом //go:build tools
// и пустыми импортами, только чтобы версии инструментов попали в go.mod.
gofmt
Главное, что он не настраивается, и это фича, а не недоработка. Один формат на всю экосистему: любой чужой код читается без адаптации, ревью не тратится на стиль, а инструменты (переписывалки, рефакторинги, go fix) безопасно меняют код. Отсюда и цитата из Go proverbs: «Gofmt's style is no one's favorite, yet gofmt is everyone's favorite». В CI проверка формата должна падать, а не предупреждать.
//go:build или суффиксом в
имени файла. Кросс-компиляция - это две переменные окружения
GOOS/GOARCH и ничего больше, пока
выключен CGO.
//go:build linux && amd64 && !nocgo
package storage
// Синтаксис с Go 1.17; старая форма // +build до сих пор встречается в чужом коде.
// Строка обязана идти до package; пустую строку после неё gofmt поставит сам.
// Поддерживает && || ! и скобки.
# Неявные теги по имени файла работают без строки //go:build
store_linux.go # GOOS=linux (и android)
store_windows_amd64.go # GOOS=windows и GOARCH=amd64
store_test.go # только при go test
# Свои теги
go build -tags="integration,e2e" ./...
go test -tags=integration ./...
# Кросс-компиляция
GOOS=linux GOARCH=amd64 go build -o app .
GOOS=darwin GOARCH=arm64 go build -o app-mac .
GOOS=windows GOARCH=amd64 go build -o app.exe .
go tool dist list # все поддерживаемые пары
# Прод-сборка контейнерного бинаря
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -trimpath -ldflags="-s -w -X main.version=$(git describe --tags)" -o /app .
| Флаг | Что делает | Зачем в проде |
|---|---|---|
CGO_ENABLED=0 |
Отключает CGO, бинарь статический |
Работает в scratch/distroless
|
-trimpath |
Убирает абсолютные пути сборки |
Воспроизводимость; в трейсах не течёт
/Users/...
|
-ldflags="-s -w" |
Выкидывает таблицу символов и DWARF | Минус ~30% размера. Но ломает читаемость стека в отладчике |
-ldflags="-X pkg.Var=val" |
Вшивает строку в переменную при линковке | Версия, коммит, дата сборки без правки кода |
-pgo=default.pgo |
Profile-guided optimization | 2–14% CPU бесплатно, если есть профиль с прода |
-buildvcs=false |
Не вшивать данные VCS | Сборка в контейнере, где git падает на чужом .git |
Ровно в одном месте, на CGO. С CGO_ENABLED=1 нужен C-компилятор и заголовки под целевую платформу, то есть полноценный кросс-тулчейн (zig cc, musl-cross, сборка в контейнере целевой платформы). Поэтому в 99% случаев ответ звучит так: «выключаю CGO, и кросс-компиляция становится одной строкой». Второй по частоте сюрприз: с CGO_ENABLED=0 работает чистый Go-резолвер DNS, который игнорирует часть настроек nsswitch.conf. В Kubernetes это обычно не проблема, в гибридных корпоративных сетях иногда всплывает.
Поставить //go:build integration в файлы, которым нужна база или брокер. Тогда go test ./... в обычном прогоне их вообще не видит (они не компилируются), а в ночном пайплайне идёт go test -tags=integration ./.... Выигрыш перед testing.Short() в том, что отсечение происходит на уровне компиляции: unit-прогон не тянет зависимости интеграционных тестов вообще.
/*
#cgo LDFLAGS: -lz
#include <zlib.h>
*/
import "C" // псевдопакет: строка import "C" должна идти сразу за комментарием
func Version() string {
return C.GoString(C.zlibVersion()) // конверсии C.GoString / C.CString
}
// C.CString выделяет память malloc'ом, освобождать её надо вручную:
// p := C.CString(s); defer C.free(unsafe.Pointer(p))
Цена - по пунктам
| Что ломается | Детали |
|---|---|
| Кросс-компиляция | Нужен C-тулчейн под каждую целевую платформу вместо двух переменных окружения |
| Статическая линковка |
Бинарь тянет glibc/musl → scratch-образ
не работает, появляется зависимость от базового
образа
|
| Производительность вызова | Каждый переход Go→C переводит горутину в состояние syscall (затянется вызов - sysmon отберёт у неё P) и переключает на системный стек. Порядок - десятки наносекунд против единиц у обычного вызова. В горячем цикле это разница в разы |
| Память и GC | Всё, что выделил C, GC не видит и не собирает. Правила передачи указателей (cgo pointer passing rules) жёсткие, нарушение ловится не всегда |
| Надёжность |
Segfault в C кладёт весь процесс,
recover тут бессилен. Стек C не растёт
как горутинный - переполнение возможно
|
| Инструменты |
-race не видит гонки внутри C,
профилировщик хуже показывает C-стеки, отладка
сложнее
|
| Сборка | Медленнее, Dockerfile сложнее, кэш хуже |
Когда CGO всё-таки нужен
- Библиотека существует только на C и переписывать её нереально: ffmpeg, OpenCV, librdkafka, ODBC-драйверы, аппаратные SDK.
- Системные API без Go-обёртки.
- Постепенная миграция legacy C-кодовой базы.
CGO_ENABLED=0 для сервисов идёт дефолтом. Если CGO действительно нужен, лучше изолировать его в отдельный бинарь или сайдкар, а не тащить в основной сервис. Два самых частых случая: mattn/go-sqlite3 требует CGO, но есть чистая замена modernc.org/sqlite; confluent-kafka-go тянет librdkafka, а segmentio/kafka-go и IBM/sarama написаны на чистом Go. Правило: прежде чем включать CGO, проверь, нет ли pure-Go альтернативы.
ServeMux; 1.23 - итераторы
range over func; 1.24 - Swiss Tables, generic
type aliases, tool в go.mod,
b.Loop; 1.25 - WaitGroup.Go и
container-aware GOMAXPROCS; 1.26 - Green Tea GC
по умолчанию, new от выражения,
errors.AsType; 1.27 - параметры типа у методов,
encoding/json/v2 отдельным пакетом,
uuid в stdlib.
Go 1.22 (февраль 2024)
-
Переменная цикла теперь своя на каждой итерации. Классическая ловушка с замыканиями и
&vисчезла. Включает её строкаgo 1.22вgo.mod: решает модуль, а не версия тулчейна, чтобы старый код не сломался молча. -
Паттерны в
http.ServeMux:"GET /users/{id}",r.PathValue("id"), многосегментный{path...}. Приоритет считается по специфичности, а конфликтующие паттерны паникуют при регистрации. -
for i := range 10: range по целому. -
math/rand/v2, первый/v2внутри самой stdlib;slices.Concat; PGO-сборки быстрее на 2–14%.
Go 1.23 (август 2024)
-
Итераторы:
rangeнад функцией + пакетiter(Seq[V],Seq2[K,V],Pull). Тело цикла становится колбэкомyield, аbreakпревращается вfalseизyield. -
Итераторные функции в
slices/maps:slices.Sorted(maps.Keys(m)),slices.All,slices.Values,slices.Collect. -
Таймеры переделаны: канал
Timer/Tickerтеперь без буфера, и неостановленные таймеры собираются GC, поэтому классическая утечкаtime.Afterвнутриselectв цикле больше не утекает. -
Пакет
uniqueдля интернирования значений.
Go 1.24 (февраль 2025)
-
Swiss Tables, новая реализация мап: группы слотов с control word, на amd64 поиск через SIMD. До 60% быстрее на микробенчмарках, около 1,5% по реальным приложениям, меньше памяти. Для кода прозрачно. Плюс новая реализация
sync.Mapи аллокатор мелких объектов. -
Generic type aliases:
type Set[T comparable] = map[T]struct{}. -
tool-директива в go.mod и командаgo tool: хак сtools.goи build-тегом больше не нужен. -
testing.B.Loop:for b.Loop()вместоfor i := 0; i < b.N; i++. Setup не попадает в замер, а аргументы и результат защищены от выбрасывания оптимизатором. -
os.Root: операции с файлами, ограниченные деревом директории (защита от path traversal); пакетweak;runtime.AddCleanupвместоSetFinalizer.
Go 1.25 (август 2025)
-
WaitGroup.Go:wg.Go(func(){...})вместо связкиAdd(1)+go+defer Done(). Убирает целый класс ошибок с забытымDoneи рассинхроном счётчика. -
Container-aware
GOMAXPROCS: рантайм читает CPU-лимит cgroup и подстраивается динамически. Библиотекаautomaxprocs, которую раньше тащили в каждый сервис, стала не нужна. -
testing/synctest, стабильный пакет для детерминированных тестов конкурентного кода с «виртуальным» временем. -
Экспериментальные на тот момент: сборщик Green Tea (
GOEXPERIMENT=greenteagc) иencoding/json/v2. Оба с тех пор стали дефолтными, см. 1.26 и 1.27. Плюс flight recorder вruntime/trace. -
net/http.CrossOriginProtection, штатная CSRF-защита на Fetch metadata (Sec-Fetch-Site), без токенов и кук.
Go 1.26 (февраль 2026)
-
Green Tea GC включён по умолчанию. Минус 10–40% накладных расходов сборщика, плюс примерно ещё 10% на свежих amd64 (Intel Ice Lake, AMD Zen 4+) за счёт векторных инструкций. Откат:
GOEXPERIMENT=nogreenteagc. -
Два изменения в самом языке. Первое:
newтеперь принимает выражение, а не один лишь тип:p := new(compute()),r := Req{Limit: new(50)}. Самописныйfunc ptr[T any](v T) *Tбольше не нужен. Второе: дженерик-тип может ссылаться на себя в собственном констрейнте:type Adder[A Adder[A]] interface{ Add(A) A }. -
errors.AsType[T](err) (T, bool), дженерик-версияerrors.As: типобезопасно, быстрее и без объявления переменной заранее. Плюсslog.NewMultiHandlerиio.ReadAllпримерно вдвое быстрее. -
Экспериментальный профиль
goroutineleak(GOEXPERIMENT=goroutineleakprofile) находит горутины, заблокированные на примитивах, до которых уже никто не дотянется. Вызовы cgo подешевели примерно на 30%. -
Мелочи, на которых ловят: редирект
ServeMuxна завершающий слэш стал 307 вместо 301;os/signal.NotifyContextотменяет контекст черезCancelCauseFunc, так чтоcontext.Causeпокажет, какой сигнал пришёл;go fixпереписан в набор «модернизаторов»;go mod initв 1.26.0 стал писать версию на минор ниже, но в 1.26.1 это откатили, и он снова пишет версию тулчейна целиком, вместе с патчем:go 1.27.1на go1.27.1.
Go 1.27 (август 2026) - текущая
-
У методов появились собственные параметры типа, то есть снято ограничение, которое годами называли главным недостатком дженериков в Go:
func (b *Box) MapTo[T any](f func(int) T) []T. Запрет остался только на стыке с интерфейсами: у метода интерфейса параметров типа быть не может, а дженерик-метод не засчитывается в реализацию интерфейса, иначе интерфейс нельзя проверить статически и собратьitab. Пример в stdlib: дженерик-методRand.N()вmath/rand/v2. -
encoding/json/v2стал отдельным пакетом, и на нём же теперь реализован старыйencoding/json(откат:GOEXPERIMENT=nojsonv2). Но поведение v1 сохранили специально: проверено на go1.27.0,json.Unmarshalпо-прежнему молча ест и{"a":1,"a":2}, и невалидный UTF-8. Строгость появляется только при явном импорте v2, поэтому апгрейд тулчейна ничего не ломает, а вот переход на v2 «сломает» приём кривого JSON, который раньше проглатывался. Unmarshal заметно быстрее. Рядом лежит низкоуровневыйencoding/json/jsontext; опции тегаformatиunknownубраны,inlineпереименован вembed. -
Пакет
uuidв стандартной библиотеке (RFC 9562):New(),NewV4(),NewV7(),Parse.NewV7сортируется по времени, ровно то, что нужно для первичных ключей в БД. Внешнийgithub.com/google/uuidбольше не обязателен. -
Профиль
goroutineleakстал общедоступным: виден вpprof.Profiles()и по/debug/pprof/goroutineleak. Штатный инструмент поиска утечек горутин в проде, а раньше обходилисьruntime.NumGoroutineиgoleakв тестах. -
Ещё из полезного:
strings.CutLast/bytes.CutLast;testing/synctest.Sleep;Server.MaxHeaderValueCountпротив заголовочного флуда; часть мелких аллокаций (<80 байт) дешевле до 30%; метки горутин изruntime/pprofпопадают в заголовки трейсбеков, и падение сразу показывает, какой запрос обрабатывала горутина. УдалёнGODEBUG=asynctimerchan: каналы таймеров теперь всегда небуферизованные.
Перечислять весь changelog не надо, это не проверяется. Достаточно назвать по 2–3 изменения, которые ты реально применил, и объяснить зачем: «в 1.22 убрали ловушку с переменной цикла, после апгрейда мы снесли из кода все v := v»; «в 1.23 появились итераторы, переписали обход пагинации, ушёл ручной курсор»; «в 1.25 GOMAXPROCS стал container-aware, выкинули automaxprocs из всех сервисов»; «в 1.27 перевели приём JSON на json/v2, а он отвергает дублирующиеся ключи, пришлось чинить одного поставщика данных». По такому ответу видно, что ты следишь за релизами и понимаешь, что они дают, а не заучил список.
Что стоит запомнить
- Пакет компилируется как единица импорта, модуль задаёт границу зависимостей.
- MVS выбирает версию по требованиям графа.
- go.sum хранит контрольные суммы.
Частые ошибки
- go.mod не фиксирует единственную версию каждого транзитивного пакета.
- Major v2+ входит в путь импорта.
Самопроверка
Сначала реши задачу самостоятельно, затем открой ответ и сравни ход рассуждений.
Чтение кода
1. Проследи значения
Что напечатает фрагмент? Объясни, почему получился такой результат.
// go.mod: module example.com/app
// файл cmd/server/main.go: package main
import "example.com/app/internal/config"
Ответ и разбор
Путь example.com/app называет модуль; cmd/server и internal/config - отдельные пакеты внутри его файловой структуры. Импорт задаётся путём пакета, а не именем репозитория как таковым.
Код-ревью
2. Найди ошибку и исправь
Зависимость имеет major version 2, но путь импорта оставлен без /v2. Исправь согласованно модуль и import.
require example.com/lib v2.3.0
import "example.com/lib"
Ответ и разбор
Для модулей v2+ major версия входит в module path. Зависимость должна объявлять module example.com/lib/v2, а потребитель импортирует example.com/lib/v2. Одной версии v2.3.0 в require недостаточно, если путь остаётся v1.
Применение
3. Реши задачу
Требования графа: приложение просит lib v1.2.0, пакет A просит lib v1.4.0. Какую версию выберет MVS?
Ответ и разбор
MVS выбирает v1.4.0 как наибольшую из минимально требуемых версий в графе. Обновление зависимости A выше этого не означает: MVS не ищет «последнюю опубликованную» версию автоматически.
Применение
4. Реши задачу
В go.sum появилась контрольная сумма транзитивной зависимости, хотя её нет в go.mod. Это само по себе ошибка? Что защищает запись?
Ответ и разбор
Само по себе это нормально: go.sum может хранить суммы модулей, загруженных при разрешении графа. Запись позволяет проверить, что полученные содержимое и go.mod совпадают с ранее подтверждёнными контрольными суммами; она не выбирает версию зависимости вместо go.mod.