Deep Engineering
Intermediate·Published·50 MIN

Packages in Go: visibility, init and project layout

Interview questions on packages climb a ladder: how to make a name private — what a package is and what the directory has to do with it — why internal — in what order init runs — why an import with an underscore — how to lay out a project. A run answers each step: the variable declared first is initialized last; package alpha is initialized before zeta although main imports zeta first; an import from internal is forbidden not by the compiler but by the go command; and one and the same visibility violation gets two different compiler messages.

Full technical treatment

TL;DR

A package is a directory of source files with one and the same name in the package line; it is the unit of visibility and the unit of initialization. From another package only names that start with an uppercase letter are visible — Go has no access modifiers. A package is imported by path (.../naming/strutil) and referred to by the name from its package line (textkit.Shout) — and these are not the same thing.

The order of initialization is set by the dependency graph, not by the order of the text. var total = price*qty, declared first, is initialized third — after price and qty. init() functions are called after all of the package's variables, in file order. A package is initialized only after everything it imports, and exactly once; among the ready ones the first by import path is taken — which is why in the run alpha is initialized before zeta, although main imports zeta first.

Beyond that comes what separates real understanding from having read about it. An import from internal/ is forbidden not by the compiler and not by the specification but by the go command: use of internal package ... not allowed. cmd/ and pkg/ are conventions nobody checks. An exported function may return a value of an unexported type — its methods can be called, the type cannot be named. One visibility violation gets two different compiler messages (an observation of gc go1.24.7, not a language rule), and which one you see depends on function inlining — substituting a function's body at the call site. The blank import _ "image/png" exists for the sake of one init that registers the decoder — without it image.Decode answers image: unknown format. And a program with a single fmt is dozens of packages, all of them initialized before main.

Where to start
Before this lesson it is enough to understand
  • how to write a function, a type and a variable at package level;
  • what a struct, its fields and a method with a receiver are — that is the lesson on structs;
  • that a Go program starts with function main in package main;
  • how to build and run a program with go build and go run — that is the lesson on the go tools (which also covers the build constraints mentioned here in passing).
  • _test.go tests and interfaces appear in a couple of places; they are covered in detail in the lessons on testing and on interfaces, which come later in the line.
You do not need to know in advance
  • one package name per directory, import path vs package name, go list;
  • export by the first letter, an unexported type in an exported API, internal/ and layout conventions;
  • the initialization order of variables, files, init() and packages, the import cycle, the blank import.

What is really being asked

The ladder is almost always this one:

  1. "How do you make a name private in Go?" — the warm-up; "lowercase first letter" is correct, but it is half the rule.
  2. "What is a package, and how is it related to a directory?" — this checks whether the candidate tells the import path from the package name.
  3. "Why the internal directory?" — and who exactly forbids such an import.
  4. "In what order does init run?" — the main question: variables, files, packages.
  5. "Why an import with an underscore?" — database/sql drivers, image formats.
  6. "How do you lay out a project?" — cmd/, internal/, pkg/: which of these is a rule and which a habit.

The ladder has one spine: every rule about packages has its own owner — the language specification, the compiler, the go command, or nobody. Export and initialization order are the language. One name per directory and internal/ are the go command. cmd/ and pkg/ are convention. A strong answer starts with who checks the rule.

Base: a package is a directory of files with one name

The specification defines a package through files, not a directory — and leaves the directory up to the implementation:

A set of files sharing the same PackageName form the implementation of a package. An implementation may require that all source files for a package inhabit the same directory.

— The Go Specification — Package clause
language contractThe rules of export, import and initialization are the language specification; one name per directory, internal/ and skipping files starting with _ are the go command's documentation (go help packages, go help gopath). The run bench/gopkg/internals.go illustrates them, it does not establish them. The numbers in block headings are the section numbers of the record runs/internals.txt, which is why they are not consecutive in the lesson. The runs are go1.24.7 and go1.25.1, linux/amd64; checked on 14.10.2026. The later Go 1.26 and 1.27 were not measured by the lesson.

The go command uses that permission: a package is a directory, and all .go files in it that take part in the build must start with the same package. Two files with package alpha and package beta in one directory do not build:

Output of bench/gopkg/internals.go, section 1 (both programs intentionally do not build); labels translated:

1) A PACKAGE IS A DIRECTORY: ONE NAME PER DIRECTORY
───────────────────────────────────────────────────
go build ./twopkg   (alpha.go: package alpha, beta.go: package beta)
    found packages alpha (alpha.go) and beta (beta.go) in <cases>/twopkg
go build ./xtestname   (util.go: package util, helper.go: package util_test)
    found packages util_test (helper.go) and util (util.go) in <cases>/xtestname

The second case hints at the only exception: a name with the _test suffix in a file that is itself named *_test.go. Such a file is an external test, a separate package in the same directory. In an ordinary file the same name is the same error.

Inside a package everything is visible — from any file, without the files importing each other. From outside only what is exported is visible. That is enough for a Junior-level answer: a package is a directory of .go files with one name in the package line. Names starting with an uppercase letter are visible from other packages, the rest only inside the package. A package is imported by path and referred to by the package name.

All of this fits into the shortest full example: a module of two packages. A module is a tree of packages under one go.mod file, which sets the prefix of import paths; details are in the lesson on modules. The three files of bench/gopkg/cases/example can be copied into an empty directory and run with go run . next to go.mod.

Full file go.mod:

TEXT
module example.com/hello
 
go 1.24

Full file greet/greet.go:

GO
// Package greet is the second package of the lesson's full example.
package greet
 
import "fmt"
 
// calls is set by init before main starts.
var calls int
 
func init() { calls = 1 }
 
// Hello is exported: its name starts with an uppercase letter.
func Hello(name string) string {
	return fmt.Sprintf("%s, %s (init ran: %v)", prefix(), name, calls == 1)
}
 
// prefix is not exported: only files of package greet can call it.
func prefix() string { return "hello" }

Full file main.go:

GO
// Full example of the lesson "Packages in Go": two packages in one module.
//
// Run in this directory (next to go.mod): go run .
package main
 
import (
	"fmt"
 
	"example.com/hello/greet"
)
 
func main() {
	fmt.Println(greet.Hello("gopher"))
}

Output of bench/gopkg/internals.go, section 14; labels translated:

14) FULL EXAMPLE: TWO PACKAGES IN ONE MODULE
────────────────────────────────────────────
cd cases/example && go list ./...
    example.com/hello
    example.com/hello/greet
cd cases/example && go run .
    hello, gopher (init ran: true)

The import path example.com/hello/greet is the module prefix plus the directory; the reference is greet.Hello, by package name. The init of package greet ran before main. And greet.prefix cannot be called from main.go — the name is lowercase; what exactly the compiler answers is shown in Mechanism 2.

One table is worth remembering right away — it answers "who checks this", which is where half of the next steps start:

rulewho owns itwhat happens on violation
export by the first letterspecification, checked by the compilercompile error
initialization orderspecification— (the order simply is that)
import cycle, import without referencesspecificationbuild error
one package name per directorygo commandfound packages ...
internal/go commanduse of internal package ... not allowed
cmd/, pkg/nobody — conventionnothing

Mechanism 1: the import path and the package name are different things

The import line names a path — where the package lives. References in code go by name — whatever stands in the package line of the imported package. The specification says exactly this: if no name is given at import, the name from the package line is used. Usually the path ends with the same word as the name — but that is an agreement, not a rule. In the run, the directory naming/strutil contains package textkit:

Output of bench/gopkg/internals.go, section 3 (wrongname intentionally does not build; the correct variant is the lines above it); labels translated:

3) IMPORT PATH AND PACKAGE NAME ARE DIFFERENT THINGS
────────────────────────────────────────────────────
go list -f '{{.ImportPath}} -> package {{.Name}}' ./naming/strutil
    deepengineering/bench/gopkg/cases/naming/strutil  ->  package textkit
go run ./naming   (import ".../naming/strutil"; textkit.Shout("hello"))
    HELLO!
go run ./naming/alias   (tk ".../naming/strutil"; tk.Shout("alias"))
    ALIAS!
go build ./naming/wrongname   (strutil.Shout — by the directory name)
    naming/wrongname/main.go:6:2: "deepengineering/bench/gopkg/cases/naming/strutil" imported as textkit and not used
    naming/wrongname/main.go:10:14: undefined: strutil

The last case is what a mismatch between path and name turns into in practice: the reader sees import ".../strutil", writes strutil.Shout and gets two errors at once. That is why the package name is kept equal to the last element of the path, and on a mismatch an explicit alias is written: tk ".../strutil".

What exactly goes into a package is decided not only by the package lines but also by file names. The fastest way to see it is go list with a template over the fields of the Package struct described by go help list:

Output of bench/gopkg/internals.go, section 2; labels translated:

2) WHAT GOES INTO A PACKAGE: go list
────────────────────────────────────
files in cases/listing:
    _draft.go api_test.go gen.go listing.go listing_linux.go listing_test.go listing_windows.go testdata/
GOOS=linux go list -f '…' ./listing
    Name=listing
    GoFiles=[listing.go listing_linux.go]
    IgnoredGoFiles=[gen.go listing_windows.go]
    TestGoFiles=[listing_test.go]
    XTestGoFiles=[api_test.go]

Seven files and a directory — and two of them made it into the package's Linux build. listing_windows.go and gen.go (with //go:build ignore) were filtered out by build constraints — those are covered in the lesson on the go tools; with GOOS=windows the same go list swaps _linux and _windows. gen.go declares package main, and there is no found packages error: the "one name per directory" rule covers only the files that made it into the build. And _draft.go and testdata/ are not mentioned anywhere — both hold something that is not Go, and go vet ./listing still passes. The go command does not read them. Per go help packages, the go command skips directories and files starting with . and _, as are directories named "testdata".

Mechanism 2: export is decided by the first letter — and only by it

Go has no public and private. Visibility outside a package is decided by one rule from the specification:

An identifier is exported if both: the first character of the identifier's name is a Unicode uppercase letter (Unicode character category Lu); and the identifier is declared in the package block or it is a field name or method name.

— The Go Specification — Exported identifiers

The package shape for the check — in it a type, a field, a method and functions are unexported, and the exported New returns an unexported type. Fragment of cases/visibility/shape/shape.go, function bodies shortened:

GO
type circle struct{ radius float64 }
type Box struct{ Width, height float64 }
 
func New(r float64) *circle       { return &circle{radius: r} }
func (c *circle) Area() float64   { ... }
func (c *circle) scale(k float64) { ... }
func area(r float64) float64      { ... }
func perimeter(r float64) float64 { ... }
func Perimeter(r float64) float64 { return perimeter(r) }
func Ωmega() string               { ... } // and ωmega, _Hidden

From another package — what is allowed:

Output of bench/gopkg/internals.go, section 4; labels translated:

4) EXPORT IS DECIDED BY THE FIRST LETTER
────────────────────────────────────────
go run ./visibility/ok
    c := shape.New(2)      type *shape.circle
    c.Area()               12.57
    var d = shape.New(1)   type *shape.circle
    b.Width, b.Height()    3 4
    {Width:3 height:4}
    Ωmega: exported (Ω is Lu)

And what is not — all nine references in one file (the program intentionally does not build). The texts are gc go1.24.7 output: an observation of a particular compiler version, not wording from the specification; go1.25.1 adds hints like (but have Ωmega) to some of the messages.

Continuation of the same section:

go build ./visibility/bad
    visibility/bad/main.go:10:20: undefined: shape.area
    visibility/bad/main.go:11:20: name perimeter not exported by package shape
    visibility/bad/main.go:12:14: name circle not exported by package shape
    visibility/bad/main.go:15:16: p.radius undefined (cannot refer to unexported field radius)
    visibility/bad/main.go:16:4: p.scale undefined (cannot refer to unexported method scale)
    visibility/bad/main.go:18:16: b.height undefined (type shape.Box has no field or method height, but does have method Height)
    visibility/bad/main.go:19:26: cannot refer to unexported field height in struct literal of type shape.Box
    visibility/bad/main.go:20:20: undefined: shape.ωmega
    visibility/bad/main.go:21:20: undefined: shape._Hidden

From these two outputs follow three things, and each is asked about separately.

An unexported type can be returned to the outside. shape.New(2) returned *shape.circle, and the exported method Area was called. What is not allowed is naming the type: name circle not exported.

Visibility is decided per name. Box is exported, its field height is not: it can be neither read nor set in a literal. fmt still prints it ({Width:3 height:4}): it reads fields through reflection, while the export rule restricts source code; how reflection itself is restricted is in the lesson on reflection.

"Uppercase" means the Unicode category Lu, not "capital Latin letter". Ωmega is exported, ωmega is not; _Hidden is not, because its first character is an underscore.

Lines 10 and 11 break the rule the same way, yet their messages differ: undefined: shape.area and name perimeter not exported by package shape. For an answer it is enough to know that both are compile errors; why the texts differ is taken apart in "Deeper".

Mechanism 3: internal/ is a go command rule, the rest are conventions

The only prohibiting rule about directory layout that anybody checks is about the internal directory. It is written not in the specification (the word internal does not appear there in that sense) but in the go command's documentation:

Code in or below a directory named "internal" is importable only by code in the directory tree rooted at the parent of "internal".

— The go command — go help gopath, Internal Directories

The layout in the run is app/internal/secret, next to it app/cmd/server and app/api, and outside app — outsider and viaapi:

Output of bench/gopkg/internals.go, section 6 (outsider and stdinternal intentionally do not build; the working path is viaapi); labels translated:

6) internal/: IMPORT ONLY FROM INSIDE THE PARENT'S TREE
───────────────────────────────────────────────────────
go run ./internalpkg/app/cmd/server   (inside app/)
    app/cmd/server: s3cr3t
go run ./internalpkg/viaapi   (outside, via app/api)
    outsider via app/api: s3cr3t
go build ./internalpkg/outsider   (outside, directly)
    package deepengineering/bench/gopkg/cases/internalpkg/outsider
        internalpkg/outsider/main.go:6:2: use of internal package deepengineering/bench/gopkg/cases/internalpkg/app/internal/secret not allowed
go build ./internalpkg/stdinternal   (import "internal/cpu")
    package deepengineering/bench/gopkg/cases/internalpkg/stdinternal
        internalpkg/stdinternal/main.go:5:2: use of internal package internal/cpu not allowed
go build ./internalpkg/app/internal/secret   (the package itself, from the command line)
    built without errors

The parent is app, not the module. outsider lives in the same module, but outside app — and is refused. internal can sit at any depth, and each one closes its own subtree; the standard library's internal/cpu sits at the root, and only the standard library itself can import it.

What is closed is the import, not the data. viaapi got the same s3cr3t through the exported app/api: internal forbids writing import, but does not stop packages inside the tree from handing out anything.

The go command checks it, not the compiler. The message is printed by the go command's package loader (cmd/go/internal/load/pkg.go, function disallowInternal, line 1472 in go1.24.7). A package named on the command line, per the comment there, is not an import, and the rule does not apply to it: that is why go build ./internalpkg/app/internal/secret built.

cmd/ and pkg/ are conventions nobody checks

Special meaning for the go command belongs, for example, to these directory names: internal, vendor, testdata and anything starting with . or _. cmd/ is not among them:

Output of bench/gopkg/internals.go, section 13 (importmain intentionally does not build); labels translated:

13) LAYOUT: cmd/ IS A CONVENTION, NOT A RULE
────────────────────────────────────────────
go build -o <tmp>/bin/ ./layout/cmd/greet ./layout/tools/anywhere
    binaries: anywhere greet
go run ./layout/tools/anywhere
    package main in tools/anywhere: builds just the same
go build ./layout/importmain   (imports package main from cmd/greet)
    layout/importmain/main.go:6:2: import "deepengineering/bench/gopkg/cases/layout/cmd/greet" is a program, not an importable package

package main in tools/anywhere built just as in cmd/greet, and the binaries are named not after the package name main but after the last element of the import path — usually the directory name. The habit of putting commands into cmd/<name>/ is useful exactly for that: the directory name becomes the binary name. The go command gives pkg/ no meaning either. The rule that does exist is the last line: package main cannot be imported, so whatever someone else needs is moved out of it into an ordinary package.

Mechanism 4: initialization order — by dependencies, not by the text

This is the main question of the ladder, and it has three levels: variables inside a package, files and init(), packages among themselves. All three are described in the specification, and on each one the "top to bottom" retelling is wrong.

Variables. At each step the variable earliest in declaration order that is already ready is taken:

Within a package, package-level variable initialization proceeds stepwise, with each step selecting the variable earliest in declaration order which has no dependencies on uninitialized variables.

— The Go Specification — Package initialization

Fragment of cases/initvars/main.go:

GO
var total = trace("total", price*qty)
var price = trace("price", 10)
var qty = trace("qty", 3)
 
func init() { fmt.Println("init()      total =", total) }

Output of bench/gopkg/internals.go, section 7; labels translated:

7) PACKAGE VARIABLES GO BY DEPENDENCIES
───────────────────────────────────────
order in the file: total = price*qty, then price, then qty
go run ./initvars
    var price = 10
    var qty   = 3
    var total = 30
    init()      total = 30
    main()

total is declared first but is not ready: it refers to price and qty. So price is initialized first, then qty, and only then total. init() is called after all of the package's variables and sees total = 30.

Files and init(). A package is initialized as a whole: first all the variables of all the files, then all init() functions — in source order, possibly in multiple files, as presented to the compiler. "As presented to the compiler" is not a figure of speech. In initfiles there are four files, each with a variable and an init(), main.go has two init() functions, and needsZ in a.go refers to fromZ in z.go:

Output of bench/gopkg/internals.go, section 8; labels translated:

8) SEVERAL FILES, SEVERAL init()
────────────────────────────────
go list -f '{{.GoFiles}}' ./initfiles
    [a.go b.go main.go z.go]
go run ./initfiles
    var fromA (a.go)
    var fromB (b.go)
    var fromMain (main.go)
    var fromZ (z.go)
    var needsZ (a.go), uses fromZ (z.go)
    init() in a.go
    init() in b.go
    init() #1 in main.go
    init() #2 in main.go
    init() in z.go
    main()

The go command presents files by name — as the specification recommends for reproducibility. List them by hand in reverse order (go run z.go main.go b.go a.go, the same section of the run) and both the variables and the init() functions flip. Two init() functions in one file are legal and both are called. And a dependency beats the file: needsZ is declared in the first file but initialized last among the variables — it was waiting for fromZ.

Packages. A package is initialized after everything it imports, and once; among the ready ones — the first by import path:

Given the list of all packages, sorted by import path, in each step the first uninitialized package in the list for which all imported packages (if any) are already initialized is initialized.

— The Go Specification — Program initialization

Output of bench/gopkg/internals.go, section 9; labels translated:

9) PACKAGES: DEPENDENCIES FIRST, EACH ONCE
──────────────────────────────────────────
main imports zeta, alpha, mid (in that order); mid imports zeta
go run ./initdeps
    alpha: var Name
    alpha: init()
    zeta: var Name
    zeta: init()
    mid: var Name, zeta.Name = zeta
    mid: init()
    main: var name
    main: init()
    main() zeta alpha mid

alpha before zeta, although main imports zeta on its first line: the order of imports in the file means nothing, the path decides. mid comes before zeta alphabetically but waits for it — mid imports it. zeta is imported by two packages and initialized once. And each package goes as a whole: variables, then init(), and only then the next package.

Output of bench/gopkg/internals.go, section 10 (the program intentionally does not build); labels translated:

10) init CANNOT BE CALLED
─────────────────────────
go build ./initcall   (main calls init())
    initcall/main.go:8:2: undefined: init

By the specification init has no parameters and no results, and the identifier init itself is not declared — the function init cannot be referred to from anywhere. So it will not return an error, and it cannot be repeated, say from a test: whatever can fail is done in an explicit function that main calls.

Mechanism 5: the blank import for the sake of init — and why cycles are forbidden

An import without references is an error. But sometimes a package is needed only for its init. For that the specification left the blank identifier:

It is illegal for a package to import itself, directly or indirectly, or to directly import a package without referring to any of its exported identifiers. To import a package solely for its side-effects (initialization), use the blank identifier as explicit package name.

— The Go Specification — Import declarations

The canonical example is image formats. image.Decode handles only the formats someone has registered, and they are registered by the init of the format's package (image/png/reader.go, lines 1052–1053: image.RegisterFormat("png", ...)):

Output of bench/gopkg/internals.go, section 12 (unused intentionally does not build; the fix is withpng); labels translated:

12) BLANK IMPORT FOR THE SAKE OF init
─────────────────────────────────────
go run ./blank/nopng   (only image imported)
    image.Decode: image: unknown format
go run ./blank/withpng   (plus import _ "image/png")
    image.Decode: png 2 x 1
go build ./blank/unused   (import "image/png" without _ and without references)
    blank/unused/main.go:5:2: "image/png" imported and not used

The same PNG, the same call; the difference is the init of package image/png. Without the underscore such an import does not build.

database/sql drivers work the same way. The standard library has no drivers, so in the run a stub fakedb stands in for one; like the real ones, it calls sql.Register in init. Continuation of section 12; labels translated:

go run ./blank/nodriver   (database/sql only)
    sql.Drivers(): []
    sql.Open("fakedb"): sql: unknown driver "fakedb" (forgotten import?)
go run ./blank/withdriver   (plus import _ ".../blank/fakedb")
    fakedb: init() -> sql.Register("fakedb", ...)
    sql.Drivers(): [fakedb]
    sql.Open("fakedb"): ok, db.Ping(): fakedb: not a real database

The hint (forgotten import?) is written into the error text right in the source (database/sql/sql.go, line 868 in go1.24.7). The flip side of the technique is visible too: init runs even if not a single function of the package is called — the import itself has consequences.

The import cycle. If orders imports users and users imports orders, neither of them can be initialized "after everything it imports". The specification forbids this, and the go command names the whole path of the cycle:

Output of bench/gopkg/internals.go, section 11 (the programs intentionally do not build); labels translated:

11) IMPORT CYCLE
────────────────
go build ./cycle/orders   (orders -> users -> orders)
    package deepengineering/bench/gopkg/cases/cycle/orders
        imports deepengineering/bench/gopkg/cases/cycle/users from orders.go
        imports deepengineering/bench/gopkg/cases/cycle/orders from users.go: import cycle not allowed
go build ./cycle/self   (a package imports itself)
    package deepengineering/bench/gopkg/cases/cycle/self
        imports deepengineering/bench/gopkg/cases/cycle/self from self.go: import cycle not allowed

The ban on cycles is what makes the "dependencies first" order always achievable. The price is in design: packages that need each other's types have to be pulled apart.

Deeper: who lines packages up, how many there are in the build, and where two messages come from

measured observationbench/gopkg/internals.go, section 9, and bench/gopkg/practice.go, section 2: go1.24.7 linux/amd64, CGO_ENABLED=1, GOFLAGS empty. GODEBUG=inittrace=1 prints a line for each package with init work; its timings are dropped from the run record, the names and order are kept. On go1.25.1 the list of packages with init work is the same, while the build graphs have more packages: 64 and 61.

The order from Mechanism 4 can be seen not only through the lines the program prints. The environment variable GODEBUG=inittrace=1 makes the runtime print a line for each package that has init work — in the order they are initialized. The end of section 9's output; labels translated:

GODEBUG=inittrace=1 ./initdeps   (packages with init work, in order)
    standard library, 14: internal/bytealg runtime math errors iter sync internal/godebug syscall time internal/poll io/fs os unicode reflect
    ours, 4: initdeps/alpha initdeps/zeta initdeps/mid main
    packages in the build in total (go list -deps): 59

Of the 59 packages in the build graph (among them the pseudo-package unsafe and main itself), 18 got a line. The rest have no init work, and the compiler does not even create a record for them (cmd/compile/internal/pkginit/init.go, lines 120–122); per the runtime package documentation, nothing is printed for packages without both user defined and compiler generated init work.

implementation detail · Go 1.24cmd/link/internal/ld/inittask.go, lines 15–39 (go1.24.7): the compiler writes an init record for each package that has something to initialize, with references to the records of the imported packages, and the linker computes an order that respects all dependencies and is otherwise lexicographic. The runtime executes the ready list.

The order is computed by the linker. Each package that has something to initialize gets, when compiled, an init record with references to the records of the packages it imports, and the linker orders all the records so as to respect the dependencies and, otherwise, lexicographically. That is the specification's rule "by import path, but dependencies first"; at startup the runtime just walks the ready list.

The "by import path" rule did not exist from the start — see "Version history" below. Hence the practical conclusion: rely only on order expressed by an import. If package B must be initialized before A, A must import B; an "alphabetical" order ties the program to directory names. And a caveat from the specification: it sees dependencies between variables only through references in the package's source. If a dependency is hidden — say, behind an interface method call — the order between such variables is not specified.

Optional experiment: how many packages are in the build graph

This is not knowledge about the language worth memorizing but an observation of one configuration: the number depends on the Go version, GOOS/GOARCH and build settings. It can be repeated with go list -deps . in the directory of a program with a single import "fmt".

Output of bench/gopkg/practice.go, section 2; labels translated:

2) EXPERIMENT: HOW MANY PACKAGES ARE IN THE BUILD GRAPH
───────────────────────────────────────────────────────
program: package main, import "fmt", fmt.Println("hello")
  configuration: go1.24.7, GOOS/GOARCH/CGO_ENABLED = linux/amd64/1, GOFLAGS empty
  packages in the build graph (go list -deps, main and unsafe included): 56
  of them with init work (GODEBUG=inittrace=1): 14
  internal/bytealg runtime math errors iter sync internal/godebug syscall time internal/poll io/fs os unicode reflect

On go1.25.1 in the same configuration there are 61 packages, and the same 14 with init work. One thing is stable here: all packages of the graph are initialized before main, and only those with something to do get an inittrace line.

One violation, two messages: export data and inlining

Back to lines 10 and 11 from Mechanism 2: both refer to an unexported function, yet the messages differ: undefined: shape.area and name perimeter not exported by package shape. Forbid inlining — with a flag for the whole build or with a //go:noinline directive on Perimeter — and the difference disappears.

Output of bench/gopkg/internals.go, section 5 (the programs intentionally do not build); labels translated:

5) ONE VIOLATION, TWO MESSAGES
──────────────────────────────
go build -gcflags=all=-l ./visibility/bad   (inlining off)
    visibility/bad/main.go:10:20: undefined: shape.area
    visibility/bad/main.go:11:20: undefined: shape.perimeter
go build ./visibility/noinline/bad   (the same pair, Perimeter marked //go:noinline)
    visibility/noinline/bad/main.go:10:20: undefined: shape.perimeter
implementation detail · Go 1.24cmd/compile/internal/types2/call.go, lines 720–728 (go1.24.7): the name is looked up in the scope of the imported package; not found — 'undefined', found but not exported — 'not exported by package'. That the unexported name gets into the export data because of the inlinable Perimeter is shown by the run: both with -gcflags=all=-l and with //go:noinline on Perimeter alone the message becomes 'undefined'. go1.25.1 adds the hint '(but have Perimeter)' to 'undefined'.

The compiler sees an imported package not through its source but through its export data — what the package reported about itself. perimeter got in there: it is called by the short Perimeter, which can be inlined into someone else's code. area is needed by nobody outside, and the compiler simply does not see it. You need to know this for one thing: undefined: pkg.name does not mean the package has no such name — it may have it, with a lowercase letter.

How to answer in an interview

Short answer: a package is a directory of files with one name in the package line. From outside only names with an uppercase first letter are visible. Initialization goes like this: first the imported packages — each once; inside a package — variables by dependencies, then all init() functions in file order; and only then main.

That is enough to answer correctly. What follows is what you add if the interviewer digs.

If the interviewer digs deeper

On "in what order does init run", show the variable declared first. "var total = price*qty is declared before price and qty, and in the run it was initialized third. The rule is the variable earliest in declaration order whose dependencies are ready. init() comes after all of the package's variables."

On "and between packages" — dependencies, then import path. "A package is initialized after everything it imports, and once. Independent ones go by import path: in the run alpha comes before zeta, although main imports zeta first. The order of imports in the file does not matter."

On "why internal" — and who checks it. "Importing a package from internal/ is allowed only to code in the tree of the parent of internal. This is a go command rule — use of internal package ... not allowed — not a language one. What is closed is the import, not the data: an exported package next to it can hand them out."

On "how to lay out a project" — separate the rule from the habit. "cmd/<name>/ for commands is a convention: the binary is named after the last element of the path, and package main builds anywhere. There are two rules: package main cannot be imported, and internal/ closes its subtree."

On "why _ in an import" — init and registration. "import _ "image/png" runs the init of the package, which registers the decoder; without it image.Decode answers image: unknown format. database/sql drivers register themselves in init the same way."

Next they ask

Next they ask

Can an exported function return a value of an unexported type?

Short answer

It can, and the compiler does not object: in the run shape.New(2) returned *shape.circle, and the caller called its exported method Area. What is not allowed is naming that type from outside (a line from section 4):

    visibility/bad/main.go:12:14: name circle not exported by package shape

So from outside you cannot write that type: in a struct field, a function parameter, var x shape.circle. A value can be obtained only with type inference — := or var d = shape.New(1). For an API this is awkward: the user cannot declare a field of that type in their own struct. That is why what goes out is usually an exported type with unexported fields, or an interface.

Next they ask

How do packages x and x_test end up in one directory?

Short answer

This is the only exception to the "one name per directory" rule. A file whose name ends in _test.go may declare package x_test — and becomes an external test: a separate package that imports x like an ordinary user and sees only what is exported. go list shows such files separately (lines from section 2):

    TestGoFiles=[listing_test.go]
    XTestGoFiles=[api_test.go]

The exception is checked by file name (go/build/build.go, lines 948–953 in go1.24.7): in a file without the _test.go suffix the same package util_test gives the ordinary error found packages util_test (helper.go) and util (util.go). An external test sees the package the way its user does — the testing package documentation calls this "black box" testing and specifies that such a test can use only its exported identifiers.

Next they ask

Two packages need each other. What do you do about the import cycle?

Short answer

First figure out what exactly is needed. If it is types — orders needs users.User, and users needs *orders.Order — they are moved into a third package that both import, and the cycle is gone. If one package needs the behavior of the other, it is replaced by an interface declared on the caller's side: the package depends on an interface it declared itself, not on the package that implements it.

Merging both packages into one is also a way out: a cycle often means the boundary between packages is drawn in the wrong place. You start from the import cycle not allowed message — it lists the whole path of the cycle with the files where the imports are.

Common misconceptions

Claim

init() is the first thing that runs in a package

Actually

First all of the package's variables are initialized, in all files, and only then are the init() functions called. In the run init() saw total = 30 — a value computed before it.

Claim

Package variables are initialized top to bottom

Actually

By dependencies: at each step — the earliest in declaration order among the ready ones. var total = price*qty, declared first, was initialized third.

Claim

Packages are initialized in the order of imports in the file

Actually

Dependencies first, independent ones by import path. main imports zeta first, and in the run alpha was initialized earlier. The order of import lines means nothing.

Claim

The package name is the last element of the import path

Actually

That is a convention. The name is whatever stands in the package line: the directory naming/strutil holds package textkit, and the reference strutil.Shout gives undefined: strutil.

Claim

internal is a compiler or language rule

Actually

The specification does not have it. The import is forbidden by the go command — the function disallowInternal in its package loader — with the message use of internal package ... not allowed. A package named on the command line is not stopped.

Claim

A Go project must have cmd/ and pkg/

Actually

Those are conventions. package main in tools/anywhere builds just as in cmd/greet, and the binary is named after the last element of the import path. Special meaning for the go command belongs, for example, to internal, vendor, testdata and names starting with . and _.

Claim

undefined: pkg.name means the package has no such name

Actually

Not necessarily: shape.area exists in the package, with a lowercase letter, and the compiler (gc go1.24.7) reported undefined. The message not exported by package appears only if the name got into the export data — like perimeter, called by the inlinable Perimeter.

Claim

init can be called by hand — say, in a test

Actually

It cannot: undefined: init. By the specification the identifier init is not declared, and the function init cannot be referred to from anywhere in a program.

Version history

VersionChangeWhat it means for code
go1.13 and earlierPer the comment in the regression test test/fixedbugs/issue31636.dir/main.go, independent packages were initialized in the order of imports in the source; the same comment says that for 1.14 the implementation moves to a variant of lexicographic ordering. The test imports c, b, a and now expects the output a b c.the order of independent packages depended on the import lines
go1.17 specificationIt says only that imported packages are initialized before the importing one, and once. There is no "by import path" rule (doc/go1.17_spec.html in the go1.24.7 distribution).the order of independent packages is not specified
go1.24.7 specification (local copy)The rule is there: from the list of packages sorted by import path, at each step — the first whose imports are already initialized. The go1.17 specification does not have it yet; in which version between them it appeared, the local sources do not say. The run on go1.24.7 and go1.25.1: alpha, zeta, mid, main.the order is specified, but rely only on imports
go1.25.1The compiler adds a hint to the undefined message: undefined: shape.perimeter (but have Perimeter) (when inlining is forbidden: -gcflags=all=-l or //go:noinline; a normal build still gives name perimeter not exported), undefined: shape.ωmega (but have Ωmega). The build graph of a program with a single fmt has five more packages than on go1.24.7.only message text and build contents
Go 1.26 and 1.27Not measured by the lesson: the runs are go1.24.7 and go1.25.1 on linux/amd64, checked on 14.10.2026. The normative source for initialization order is the current specification, section Program initialization; diagnostic texts and the contents of the build graph may differ in newer versions.rules — per the specification; numbers and message texts — measure yourself

Practice

Two tasks. Answer first, then check against the real output: in both the correct answer is taken from a run of the script, not assigned.

Practice · predict the output

A program of two packages: main imports dep. In main the variable total is declared before price and qty, on which it depends, and there are two init functions. What does the program print?
// 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") }

// 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) }

Practice · estimate

Packages a, b and c import a shared package reg; main imports a, b and c. Each of the five packages has exactly one init function that prints one line. How many lines do the init functions print before main starts?
lines

Knowledge check

Question 1 of 5

Package shape declares a function area with a lowercase letter. What happens if shape.area(1) is called from another package?

Sources & further reading

6 SOURCES

  1. The Go Specification — Exported identifiersOfficial documentation. The whole export rule: «An identifier is exported if both: the first character of the identifier's name is a Unicode uppercase letter (Unicode character category Lu); and the identifier is declared in the package block or it is a field name or method name». And: «All other identifiers are not exported». go_spec.html go1.24.7, lines 2396–2411.https://go.dev/ref/spec#Exported_identifiers
  2. The Go Specification — Package clause and Import declarationsOfficial documentation. What a package is: «A set of files sharing the same PackageName form the implementation of a package. An implementation may require that all source files for a package inhabit the same directory». What is forbidden in imports and why the blank identifier: «It is illegal for a package to import itself, directly or indirectly, or to directly import a package without referring to any of its exported identifiers. To import a package solely for its side-effects (initialization), use the blank identifier as explicit package name». go_spec.html go1.24.7, lines 8013–8014 and 8084–8088.https://go.dev/ref/spec#Import_declarations
  3. The Go Specification — Package initialization and Program initializationOfficial documentation. Variables: «Within a package, package-level variable initialization proceeds stepwise, with each step selecting the variable earliest in declaration order which has no dependencies on uninitialized variables». init: «The entire package is initialized by assigning initial values to all its package-level variables followed by calling all init functions in the order they appear in the source, possibly in multiple files, as presented to the compiler». Packages: «If multiple packages import a package, the imported package will be initialized only once» and «Given the list of all packages, sorted by import path, in each step the first uninitialized package in the list for which all imported packages (if any) are already initialized is initialized». go_spec.html go1.24.7, lines 8194–8385.https://go.dev/ref/spec#Package_initialization
  4. The go command — Internal Directories (go help gopath)Official documentation. The internal rule is written in the go command's documentation, not in the specification: «Code in or below a directory named "internal" is importable only by code in the directory tree rooted at the parent of "internal"». The text is cmd/go/internal/help/helpdoc.go, line 391 (go1.24.7); the check is cmd/go/internal/load/pkg.go, function disallowInternal, line 1472.https://pkg.go.dev/cmd/go#hdr-Internal_Directories
  5. The go command — Package lists and patterns (go help packages)Official documentation. A package name need not be unique, and package main cannot be imported: «Packages in a program need not have unique package names, but there are two reserved package names with special meaning. The name main indicates a command, not a library. Commands are built into binaries and cannot be imported». And what the go command skips: «Directory and file names that begin with "." or "_" are ignored by the go tool, as are directories named "testdata"».https://pkg.go.dev/cmd/go#hdr-Package_lists_and_patterns
  6. The image packageOfficial documentation. Why the standard library uses a blank import: «Decoding any particular image format requires the prior registration of a decoder function. Registration is typically automatic as a side effect of initializing that format's package so that, to decode a PNG image, it suffices to have import _ "image/png" in a program's main package». The registration is image/png/reader.go, lines 1052–1053.https://pkg.go.dev/image