MEASUREMENT
bench/gopkg/practice.go
The script that produced the numbers in the article, and the record of the run. The file is read from the repository at build time — this is the code that was run, not a copy of it.
- Cited in
- /en/interview/golang/packages
- How to run it
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
The run below is recorded in Russian. It is a lab record, kept in the language it was written in; the numbers, the tables and the code read the same either way.
Record of the run
Замеры для урока «Пакеты в 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. Тексты диагностик и число пакетов в графе сборки — наблюдения этих версий, а не правила языка.
Script
231 lines//go:build ignore
// Две задачи урока про пакеты и один факультативный эксперимент: всё
// отвечает прогоном.
//
// ЗАДАЧА 1 — ПРЕДСКАЗАТЬ ВЫВОД. Программа из двух пакетов: main импортирует
// dep, в main переменные объявлены не в том порядке, в каком зависят друг от
// друга, и функций init две. Вывод отвечает на три вопроса сразу: кто раньше —
// зависимость или импортирующий пакет; в каком порядке инициализируются
// переменные; когда вызываются init. Программа записывается во временный
// каталог из строк ниже и запускается командой go — поэтому показанный в
// уроке код дословно есть в этом файле.
//
// ЭКСПЕРИМЕНТ (раздел 2, не задача). Сколько пакетов войдёт в программу,
// которая импортирует только fmt, и сколько из них делают работу
// инициализации. Число зависит от версии Go и конфигурации сборки, поэтому
// печатается вместе с ней: это не знание о языке, а наблюдение.
//
// ЗАДАЧА 2 (раздел 3) — ОЦЕНИТЬ ЧИСЛО. Пакеты a, b и c импортируют общий
// пакет reg, main импортирует a, b и c; в каждом из пяти пакетов одна init,
// которая печатает строку. Сколько строк init будет до main? Ответ держится
// на правиле «каждый пакет инициализируется один раз», а не на памяти.
//
// ЗАПУСК:
//
// go run bench/gopkg/practice.go
// /usr/local/go1.25.1/bin/go run bench/gopkg/practice.go
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
)
// ---------------------------------------------------------------- задача 1
const goMod = `module example
go 1.24
`
const depGo = `
// dep/dep.go
package dep
import "fmt"
var Ready = mark("dep: var Ready")
func mark(s string) bool {
fmt.Println(s)
return true
}
func init() { fmt.Println("dep: init") }
`
const mainGo = `
// main.go
package main
import (
"fmt"
"example/dep"
)
var total = trace("total", price*qty)
var price = trace("price", 10)
var qty = trace("qty", 3)
func trace(name string, v int) int {
fmt.Println("main: var", name)
return v
}
func init() { fmt.Println("main: init 1, total =", total) }
func init() { fmt.Println("main: init 2") }
func main() { fmt.Println("main: main, dep.Ready =", dep.Ready) }
`
// ---------------------------------------------------------------- эксперимент
const helloGo = `package main
import "fmt"
func main() { fmt.Println("hello") }
`
// ---------------------------------------------------------------- задача 2
const regGo = `package reg
import "fmt"
func init() { fmt.Println("init reg") }
func Touch() {}
`
// leafGo — пакеты a, b и c: каждый импортирует reg.
const leafGo = `package NAME
import (
"fmt"
"example/reg"
)
func init() {
reg.Touch()
fmt.Println("init NAME")
}
`
const shareMain = `package main
import (
"fmt"
_ "example/a"
_ "example/b"
_ "example/c"
)
func init() { fmt.Println("init main") }
func main() { fmt.Println("main") }
`
var goBin string
func write(dir, name, body string) {
p := filepath.Join(dir, name)
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
panic(err)
}
if err := os.WriteFile(p, []byte(body), 0o644); err != nil {
panic(err)
}
}
func command(dir string, env []string, name string, args ...string) (stdout, stderr string) {
cmd := exec.Command(name, args...)
cmd.Dir = dir
cmd.Env = append(append(os.Environ(), "GOTOOLCHAIN=local", "GOFLAGS="), env...)
var so, se strings.Builder
cmd.Stdout, cmd.Stderr = &so, &se
if err := cmd.Run(); err != nil {
panic(fmt.Sprint(err, se.String()))
}
return so.String(), se.String()
}
func show(title string) {
fmt.Println()
fmt.Println(title)
fmt.Println(strings.Repeat("─", len([]rune(title))))
}
func main() {
goBin = filepath.Join(runtime.GOROOT(), "bin", "go")
if _, err := os.Stat(goBin); err != nil {
goBin = "go"
}
work, err := os.MkdirTemp("", "gopkg-practice")
if err != nil {
panic(err)
}
defer os.RemoveAll(work)
fmt.Printf("%s %s/%s | пакеты: задачи урока и эксперимент\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
show("1) ПОРЯДОК ИНИЦИАЛИЗАЦИИ: ВЫВОД ПРОГРАММЫ")
p1 := filepath.Join(work, "p1")
write(p1, "go.mod", goMod)
write(p1, "dep/dep.go", depGo)
write(p1, "main.go", mainGo)
out, _ := command(p1, nil, goBin, "run", ".")
fmt.Print(out)
show("2) ЭКСПЕРИМЕНТ: СКОЛЬКО ПАКЕТОВ В ГРАФЕ СБОРКИ")
p2 := filepath.Join(work, "p2")
write(p2, "go.mod", goMod)
write(p2, "main.go", helloGo)
deps, _ := command(p2, nil, goBin, "list", "-deps", ".")
n := len(strings.Fields(deps))
bin := filepath.Join(work, "hello")
command(p2, nil, goBin, "build", "-o", bin, ".")
_, trace := command(p2, []string{"GODEBUG=inittrace=1"}, bin)
re := regexp.MustCompile(`(?m)^init (\S+) @`)
var withWork []string
for _, m := range re.FindAllStringSubmatch(trace, -1) {
withWork = append(withWork, m[1])
}
env, _ := command(p2, nil, goBin, "env", "GOOS", "GOARCH", "CGO_ENABLED")
fmt.Println("программа: package main, import \"fmt\", fmt.Println(\"hello\")")
fmt.Printf(" конфигурация: %s, GOOS/GOARCH/CGO_ENABLED = %s, GOFLAGS пуст\n",
runtime.Version(), strings.Join(strings.Fields(env), "/"))
fmt.Printf(" пакетов в графе сборки (go list -deps, вместе с main и unsafe): %d\n", n)
fmt.Printf(" из них с работой инициализации (GODEBUG=inittrace=1): %d\n", len(withWork))
fmt.Printf(" %s\n", strings.Join(withWork, " "))
show("3) ОБЩАЯ ЗАВИСИМОСТЬ: СКОЛЬКО СТРОК init ДО main")
p3 := filepath.Join(work, "p3")
write(p3, "go.mod", goMod)
write(p3, "reg/reg.go", regGo)
for _, name := range []string{"a", "b", "c"} {
write(p3, name+"/"+name+".go", strings.ReplaceAll(leafGo, "NAME", name))
}
write(p3, "main.go", shareMain)
out, _ = command(p3, nil, goBin, "run", ".")
fmt.Println("a, b, c импортируют reg; main импортирует a, b, c; в каждом пакете одна init")
fmt.Print(out)
inits := 0
for _, l := range strings.Split(out, "\n") {
if strings.HasPrefix(l, "init ") {
inits++
}
}
fmt.Printf(" строк init до main: %d (пакетов: 5, импортов reg: 3)\n", inits)
}