ЗАМЕР
bench/gopkg/internals.go
Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.
- Цитируется в статье
- /ru/interview/golang/packages
- Как запустить
go run bench/gopkg/internals.go /usr/local/go1.25.1/bin/go run bench/gopkg/internals.go go run bench/gopkg/practice.go /usr/local/go1.25.1/bin/go run bench/gopkg/practice.go
Запись прогона
Замеры для урока «Пакеты в Go»
| Файл | Что делает |
|---|---|
internals.go |
четырнадцать наблюдений, каждое — сборка, запуск или go list подопытной программы из cases/: одно имя пакета на каталог, что входит в пакет, путь импорта против имени пакета, экспорт по первой букве и два разных сообщения компилятора на одно нарушение, internal/, порядок инициализации переменных, файлов и init(), порядок пакетов и GODEBUG=inittrace=1, init нельзя вызвать, цикл импортов, пустой импорт ради init (image/png, драйвер database/sql), cmd/ как соглашение и пакет main, который нельзя импортировать; полный пример из двух пакетов |
practice.go |
источник ответов двух практических задач урока: вывод программы из двух пакетов с init и переменными не по порядку (раздел 1); сколько строк init до main, когда три пакета импортируют общий reg (раздел 3). Раздел 2 — факультативный эксперимент: сколько пакетов в графе сборки программы с одним fmt, с конфигурацией сборки |
cases/ |
отдельный модуль deepengineering/bench/gopkg/cases; часть программ намеренно не собирается |
cases/twopkg, cases/xtestname |
два имени пакета в одном каталоге; package util_test в файле без суффикса _test.go |
cases/listing |
пакет с файлами под две ОС, //go:build ignore, внутренним и внешним тестом, _draft.go и testdata/ |
cases/naming |
каталог strutil с пакетом textkit; импорт с псевдонимом; обращение по имени каталога |
cases/visibility |
shape с неэкспортируемыми типом, полем, методом и функциями; ok пользуется разрешённым, bad — запрещённым; noinline — та же пара perimeter/Perimeter с //go:noinline |
cases/internalpkg |
app/internal/secret и кто может его импортировать; internal/cpu из стандартной библиотеки |
cases/initvars, cases/initfiles, cases/initdeps, cases/initcall |
порядок инициализации: переменные, файлы и init(), пакеты; вызов init() |
cases/cycle |
два пакета, импортирующих друг друга, и пакет, импортирующий сам себя |
cases/blank |
image.Decode без image/png и с ним; неиспользуемый импорт; драйвер-заглушка fakedb, регистрирующийся в init |
cases/example |
полный пример урока — отдельный модуль example.com/hello (go.mod, main.go, greet/greet.go); go run . в этом каталоге, раздел 14 |
cases/layout |
package main в cmd/greet и в tools/anywhere; попытка импортировать пакет main |
go run bench/gopkg/internals.go
/usr/local/go1.25.1/bin/go run bench/gopkg/internals.go
go run bench/gopkg/practice.go
/usr/local/go1.25.1/bin/go run bench/gopkg/practice.go
Записи прогонов — в runs/: internals.txt и practice.txt — go1.24.7
linux/amd64, *-go1.25.txt — go1.25.1. Абсолютный путь к cases/ заменён
плейсхолдером <cases>, временный каталог — <tmp>. Времена из
GODEBUG=inittrace=1 не печатаются: остаются только имена пакетов и их порядок.
Что получилось
Пакет — это каталог, и имя у него одно. Два файла с package alpha и
package beta в одном каталоге: found packages alpha (alpha.go) and beta (beta.go). Исключение только одно — package util_test в файле
_test.go; в обычном файле тот же util_test даёт ту же ошибку.
Путь импорта и имя пакета — разные вещи. Каталог naming/strutil
содержит package textkit; импортируют его по пути, обращаются — textkit.Shout.
Обращение strutil.Shout даёт две ошибки: imported as textkit and not used и
undefined: strutil.
Экспорт решает первая буква — и только категория Lu. Из другого пакета
нельзя обратиться к area, circle, полю radius, методу scale, полю
height (в том числе в составном литерале), к ωmega и _Hidden; Ωmega
экспортируется. Но shape.New(2) возвращает значение неэкспортируемого типа
*shape.circle, и его метод Area вызывается.
Одно нарушение — два сообщения. shape.area даёт undefined: shape.area,
shape.perimeter — name perimeter not exported by package shape. Разница —
в том, попало ли имя в данные экспорта: perimeter вызывается из встраиваемой
Perimeter. С -gcflags=all=-l оба — undefined; то же даёт //go:noinline на одной Perimeter (cases/visibility/noinline). go1.25.1 добавляет
подсказку (but have Perimeter).
internal/ проверяет команда go. Импорт app/internal/secret из
app/cmd/server собирается, из outsider — use of internal package … not allowed; то же для internal/cpu. Через экспортируемый app/api — работает.
Сам пакет, названный в командной строке, собирается.
Переменные — по зависимостям, init — по файлам. total = price*qty,
объявленная первой, инициализируется последней. Переменные нескольких файлов
идут в порядке a.go b.go main.go z.go — так их подаёт компилятору команда go, и тот же список показывает go list, — кроме
needsZ из a.go, которая ждёт fromZ из z.go. go run z.go main.go b.go a.go переворачивает порядок и переменных, и init. Две init в одном файле
вызываются обе, по порядку.
Пакеты — зависимости раньше, остальное по пути импорта, каждый один раз.
main импортирует zeta, alpha, mid; mid импортирует zeta. Порядок:
alpha, zeta, mid, main; zeta инициализирован один раз. Из 59 пакетов
сборки работу инициализации делают 14 пакетов стандартной библиотеки и 4
наших.
init нельзя вызвать, цикл импортов запрещён. undefined: init;
import cycle not allowed — и для двух пакетов, и для пакета, импортирующего
сам себя.
Пустой импорт — ради init. Без image/png image.Decode отвечает
image: unknown format, с import _ "image/png" — png 2 x 1. Обычный импорт
без обращений не собирается. Драйвер-заглушка регистрируется в init:
sql.Drivers() — [] без него и [fakedb] с ним; без импорта sql.Open
отвечает unknown driver "fakedb" (forgotten import?).
cmd/ — соглашение. package main в tools/anywhere собирается так же,
как в cmd/greet; бинарники называются по последнему элементу пути импорта — здесь это имя каталога. Пакет main импортировать
нельзя: is a program, not an importable package.
Программа с одним fmt — 56 пакетов в графе go list -deps, считая
main и псевдопакет unsafe (go1.25.1 — 61); работу инициализации делают
14 из них.
Общая зависимость инициализируется один раз (practice.go, раздел 3).
a, b и c импортируют reg, main импортирует их: строк init до
main — 5, а не 7; reg — первым, main — последним.
Полный пример (cases/example, раздел 14): go run . печатает
hello, gopher (init ran: true).
Прогоны сверены 14.10.2026 на go1.24.7 и go1.25.1, linux/amd64. Тексты диагностик и число пакетов в графе сборки — наблюдения этих версий, а не правила языка.
Скрипт
333 строк//go:build ignore
// Пакеты в Go: что из правил видимости, инициализации и раскладки проверяет
// компилятор, что — команда go, а что остаётся соглашением.
//
// ЗАЧЕМ ЭТА ПРОГРАММА. Вопросы о пакетах на собеседовании звучат как
// справочные — «как сделать имя приватным», «в каком порядке вызываются
// init», «зачем internal». Отличает знающего не формулировка правила, а
// точность: какое сообщение напишет компилятор, кто именно запрещает импорт
// internal, почему переменная, объявленная первой, инициализируется
// последней. Здесь каждое такое поведение получено сборкой или запуском
// подопытной программы из cases/, а не пересказано.
//
// ЧЕТЫРНАДЦАТЬ СЮЖЕТОВ:
//
// 1. Пакет — это каталог: два имени пакета в одном каталоге — ошибка.
// 2. Что входит в пакет: go list по каталогу с тестами, файлами под другую
// ОС, //go:build ignore, _draft.go и testdata.
// 3. Путь импорта и имя пакета — разные вещи.
// 4. Экспорт по первой букве: что можно и чего нельзя из другого пакета.
// 5. Одно нарушение — два разных сообщения компилятора (и почему).
// 6. internal/: кому импорт разрешён, кому нет.
// 7. Переменные пакета инициализируются по зависимостям, а не по порядку в
// файле.
// 8. Несколько файлов и несколько init(): порядок — как файлы поданы
// компилятору.
// 9. Пакеты: зависимости раньше, каждый один раз, остальное — по пути
// импорта; и что показывает GODEBUG=inittrace=1.
// 10. init нельзя вызвать.
// 11. Цикл импортов.
// 12. Пустой импорт ради init: image/png и драйвер database/sql.
// 13. Раскладка: cmd/ — соглашение; пакет main нельзя импортировать.
// 14. Полный пример: модуль cases/example из двух пакетов, go run .
//
// Подопытные программы лежат в cases/ — отдельный модуль со своим go.mod.
// Часть из них намеренно не собирается. Абсолютный путь к cases/ и
// временные каталоги в выводе заменены плейсхолдерами <cases> и <tmp>.
//
// ЗАПУСК:
//
// go run bench/gopkg/internals.go
// /usr/local/go1.25.1/bin/go run bench/gopkg/internals.go
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
)
var (
cases string // bench/gopkg/cases
work string // временный каталог для бинарников
goBin string // go той же версии, что запустила программу
)
type result struct {
out string // stdout и stderr вместе
err string // только stderr (когда собран отдельно)
code int
}
// run запускает команду в каталоге cases с добавочными переменными окружения.
func run(env []string, name string, args ...string) result {
cmd := exec.Command(name, args...)
cmd.Dir = cases
cmd.Env = append(append(os.Environ(), "GOTOOLCHAIN=local", "GOFLAGS="), env...)
out, err := cmd.CombinedOutput()
code := 0
if ee, ok := err.(*exec.ExitError); ok {
code = ee.ExitCode()
} else if err != nil {
panic(err)
}
return result{out: string(out), code: code}
}
// runSplit — то же, но stdout и stderr раздельно.
func runSplit(env []string, name string, args ...string) result {
cmd := exec.Command(name, args...)
cmd.Dir = cases
cmd.Env = append(append(os.Environ(), "GOTOOLCHAIN=local", "GOFLAGS="), env...)
var so, se strings.Builder
cmd.Stdout, cmd.Stderr = &so, &se
err := cmd.Run()
code := 0
if ee, ok := err.(*exec.ExitError); ok {
code = ee.ExitCode()
} else if err != nil {
panic(err)
}
return result{out: so.String(), err: se.String(), code: code}
}
func goCmd(args ...string) result { return run(nil, goBin, args...) }
func clean(s string) string {
s = strings.ReplaceAll(s, cases, "<cases>")
s = strings.ReplaceAll(s, work, "<tmp>")
return s
}
// lines печатает строки вывода с отступом, пропуская строки «# пакет».
func lines(out string, indent string) {
for _, l := range strings.Split(strings.TrimRight(out, "\n"), "\n") {
if strings.HasPrefix(l, "# ") || strings.TrimSpace(l) == "" {
continue
}
l = strings.ReplaceAll(strings.TrimRight(clean(l), " \t"), "\t", " ")
fmt.Println(indent + l)
}
}
func show(title string) {
fmt.Println()
fmt.Println(title)
fmt.Println(strings.Repeat("─", len([]rune(title))))
}
// build собирает пакет и печатает либо «собрано», либо ошибки компилятора.
func build(label string, args ...string) {
r := goCmd(append([]string{"build", "-o", os.DevNull}, args...)...)
fmt.Println(label)
if r.code == 0 {
fmt.Println(" собрано без ошибок")
return
}
lines(r.out, " ")
}
// gorun запускает пакет и печатает его вывод.
func gorun(label string, pkg string) {
r := goCmd("run", pkg)
fmt.Println(label)
lines(r.out, " ")
}
const mod = "deepengineering/bench/gopkg/cases/"
func main() {
_, file, _, _ := runtime.Caller(0)
cases = filepath.Join(filepath.Dir(file), "cases")
goBin = filepath.Join(runtime.GOROOT(), "bin", "go")
if _, err := os.Stat(goBin); err != nil {
goBin = "go"
}
var err error
work, err = os.MkdirTemp("", "gopkg")
if err != nil {
panic(err)
}
defer os.RemoveAll(work)
fmt.Printf("%s %s/%s | пакеты: видимость, init, internal, раскладка\n",
runtime.Version(), runtime.GOOS, runtime.GOARCH)
// ------------------------------------------------------------------ 1
show("1) ПАКЕТ — ЭТО КАТАЛОГ: ОДНО ИМЯ НА КАТАЛОГ")
build("go build ./twopkg (alpha.go: package alpha, beta.go: package beta)", "./twopkg")
build("go build ./xtestname (util.go: package util, helper.go: package util_test)", "./xtestname")
// ------------------------------------------------------------------ 2
show("2) ЧТО ВХОДИТ В ПАКЕТ: go list")
fmt.Println("файлы в cases/listing:")
ents, _ := os.ReadDir(filepath.Join(cases, "listing"))
var names []string
for _, e := range ents {
n := e.Name()
if e.IsDir() {
n += "/"
}
names = append(names, n)
}
fmt.Println(" " + strings.Join(names, " "))
for _, goos := range []string{"linux", "windows"} {
r := run([]string{"GOOS=" + goos}, goBin, "list", "-f",
"Name={{.Name}}\nGoFiles={{.GoFiles}}\nIgnoredGoFiles={{.IgnoredGoFiles}}\nTestGoFiles={{.TestGoFiles}}\nXTestGoFiles={{.XTestGoFiles}}",
"./listing")
fmt.Printf("GOOS=%s go list -f '…' ./listing\n", goos)
lines(r.out, " ")
}
r := goCmd("vet", "./listing")
fmt.Printf("go vet ./listing: код выхода %d, вывод %q\n", r.code, strings.TrimSpace(r.out))
// ------------------------------------------------------------------ 3
show("3) ПУТЬ ИМПОРТА И ИМЯ ПАКЕТА — РАЗНЫЕ ВЕЩИ")
r = goCmd("list", "-f", "{{.ImportPath}} -> package {{.Name}}", "./naming/strutil")
fmt.Println("go list -f '{{.ImportPath}} -> package {{.Name}}' ./naming/strutil")
lines(r.out, " ")
gorun("go run ./naming (import \".../naming/strutil\"; textkit.Shout(\"hello\"))", "./naming")
gorun("go run ./naming/alias (tk \".../naming/strutil\"; tk.Shout(\"alias\"))", "./naming/alias")
build("go build ./naming/wrongname (strutil.Shout — по имени каталога)", "./naming/wrongname")
// ------------------------------------------------------------------ 4
show("4) ЭКСПОРТ — ПО ПЕРВОЙ БУКВЕ")
gorun("go run ./visibility/ok", "./visibility/ok")
build("go build ./visibility/bad", "./visibility/bad")
// ------------------------------------------------------------------ 5
show("5) ОДНО НАРУШЕНИЕ — ДВА СООБЩЕНИЯ")
for _, flags := range [][]string{nil, {"-gcflags=all=-l"}} {
args := append(append([]string{}, flags...), "./visibility/bad")
r := goCmd(append([]string{"build", "-o", os.DevNull}, args...)...)
label := "go build ./visibility/bad"
if flags != nil {
label = "go build -gcflags=all=-l ./visibility/bad (встраивание выключено)"
}
fmt.Println(label)
for _, l := range strings.Split(r.out, "\n") {
if strings.Contains(l, "area") || strings.Contains(l, "perimeter") {
fmt.Println(" " + l)
}
}
}
build("go build ./visibility/noinline/bad (та же пара, Perimeter помечена //go:noinline)", "./visibility/noinline/bad")
// ------------------------------------------------------------------ 6
show("6) internal/: ИМПОРТ ТОЛЬКО ИЗНУТРИ ДЕРЕВА РОДИТЕЛЯ")
gorun("go run ./internalpkg/app/cmd/server (внутри app/)", "./internalpkg/app/cmd/server")
gorun("go run ./internalpkg/viaapi (снаружи, через app/api)", "./internalpkg/viaapi")
build("go build ./internalpkg/outsider (снаружи, напрямую)", "./internalpkg/outsider")
build("go build ./internalpkg/stdinternal (import \"internal/cpu\")", "./internalpkg/stdinternal")
build("go build ./internalpkg/app/internal/secret (сам пакет — из командной строки)", "./internalpkg/app/internal/secret")
// ------------------------------------------------------------------ 7
show("7) ПЕРЕМЕННЫЕ ПАКЕТА — ПО ЗАВИСИМОСТЯМ")
fmt.Println("порядок в файле: total = price*qty, потом price, потом qty")
gorun("go run ./initvars", "./initvars")
// ------------------------------------------------------------------ 8
show("8) НЕСКОЛЬКО ФАЙЛОВ, НЕСКОЛЬКО init()")
r = goCmd("list", "-f", "{{.GoFiles}}", "./initfiles")
fmt.Println("go list -f '{{.GoFiles}}' ./initfiles")
lines(r.out, " ")
gorun("go run ./initfiles", "./initfiles")
{
cmd := exec.Command(goBin, "run", "z.go", "main.go", "b.go", "a.go")
cmd.Dir = filepath.Join(cases, "initfiles")
cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local", "GOFLAGS=")
out, _ := cmd.CombinedOutput()
fmt.Println("go run z.go main.go b.go a.go (файлы — в обратном порядке)")
lines(string(out), " ")
}
// ------------------------------------------------------------------ 9
show("9) ПАКЕТЫ: ЗАВИСИМОСТИ РАНЬШЕ, КАЖДЫЙ ОДИН РАЗ")
fmt.Println("main импортирует zeta, alpha, mid (в этом порядке); mid импортирует zeta")
gorun("go run ./initdeps", "./initdeps")
bin := filepath.Join(work, "initdeps")
if r := goCmd("build", "-o", bin, "./initdeps"); r.code != 0 {
panic(r.out)
}
tr := runSplit([]string{"GODEBUG=inittrace=1"}, bin)
reInit := regexp.MustCompile(`^init (\S+) @`)
var std, ours []string
for _, l := range strings.Split(tr.err, "\n") {
m := reInit.FindStringSubmatch(l)
if m == nil {
continue
}
if strings.HasPrefix(m[1], mod) || m[1] == "main" {
ours = append(ours, strings.TrimPrefix(m[1], mod))
} else {
std = append(std, m[1])
}
}
deps := goCmd("list", "-deps", "./initdeps")
nDeps := len(strings.Fields(deps.out))
fmt.Println("GODEBUG=inittrace=1 ./initdeps (пакеты с работой инициализации, по порядку)")
fmt.Printf(" стандартная библиотека, %d: %s\n", len(std), strings.Join(std, " "))
fmt.Printf(" наши, %d: %s\n", len(ours), strings.Join(ours, " "))
fmt.Printf(" всего пакетов в сборке (go list -deps): %d\n", nDeps)
// ------------------------------------------------------------------ 10
show("10) init НЕЛЬЗЯ ВЫЗВАТЬ")
build("go build ./initcall (main вызывает init())", "./initcall")
// ------------------------------------------------------------------ 11
show("11) ЦИКЛ ИМПОРТОВ")
build("go build ./cycle/orders (orders -> users -> orders)", "./cycle/orders")
build("go build ./cycle/self (пакет импортирует сам себя)", "./cycle/self")
// ------------------------------------------------------------------ 12
show("12) ПУСТОЙ ИМПОРТ РАДИ init")
gorun("go run ./blank/nopng (импортирован только image)", "./blank/nopng")
gorun("go run ./blank/withpng (плюс import _ \"image/png\")", "./blank/withpng")
build("go build ./blank/unused (import \"image/png\" без _ и без обращений)", "./blank/unused")
gorun("go run ./blank/nodriver (только database/sql)", "./blank/nodriver")
gorun("go run ./blank/withdriver (плюс import _ \".../blank/fakedb\")", "./blank/withdriver")
// ------------------------------------------------------------------ 13
show("13) РАСКЛАДКА: cmd/ — СОГЛАШЕНИЕ, А НЕ ПРАВИЛО")
outDir := filepath.Join(work, "bin") + string(os.PathSeparator)
os.MkdirAll(outDir, 0o755)
r = goCmd("build", "-o", outDir, "./layout/cmd/greet", "./layout/tools/anywhere")
if r.code != 0 {
panic(r.out)
}
bins, _ := os.ReadDir(outDir)
var bn []string
for _, b := range bins {
bn = append(bn, b.Name())
}
fmt.Println("go build -o <tmp>/bin/ ./layout/cmd/greet ./layout/tools/anywhere")
fmt.Println(" бинарники: " + strings.Join(bn, " "))
gorun("go run ./layout/tools/anywhere", "./layout/tools/anywhere")
build("go build ./layout/importmain (импортирует пакет main из cmd/greet)", "./layout/importmain")
// ------------------------------------------------------------------ 14
// Полный пример урока — отдельный модуль cases/example со своим go.mod:
// его можно скопировать в пустой каталог и запустить той же командой.
show("14) ПОЛНЫЙ ПРИМЕР: ДВА ПАКЕТА В ОДНОМ МОДУЛЕ")
ex := filepath.Join(cases, "example")
exCmd := func(args ...string) string {
cmd := exec.Command(goBin, args...)
cmd.Dir = ex
cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local", "GOFLAGS=")
out, err := cmd.CombinedOutput()
if err != nil {
panic(string(out))
}
return string(out)
}
fmt.Println("cd cases/example && go list ./...")
lines(exCmd("list", "./..."), " ")
fmt.Println("cd cases/example && go run .")
lines(exCmd("run", "."), " ")
}