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.
- 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
mainin packagemain; - how to build and run a program with
go buildandgo run— that is the lesson on the go tools (which also covers the build constraints mentioned here in passing). _test.gotests 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.
- 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:
- "How do you make a name private in Go?" — the warm-up; "lowercase first letter" is correct, but it is half the rule.
- "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.
- "Why the
internaldirectory?" — and who exactly forbids such an import. - "In what order does
initrun?" — the main question: variables, files, packages. - "Why an import with an underscore?" —
database/sqldrivers,imageformats. - "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 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:
module example.com/hello
go 1.24Full file greet/greet.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:
// 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:
| rule | who owns it | what happens on violation |
|---|---|---|
| export by the first letter | specification, checked by the compiler | compile error |
| initialization order | specification | — (the order simply is that) |
| import cycle, import without references | specification | build error |
| one package name per directory | go command | found packages ... |
internal/ | go command | use of internal package ... not allowed |
cmd/, pkg/ | nobody — convention | nothing |
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 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:
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, _HiddenFrom 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 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.
Fragment of cases/initvars/main.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.
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 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
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
.
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
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
Can an exported function return a value of an unexported type?
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.
How do packages x and x_test end up in one directory?
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
.
Two packages need each other. What do you do about the import cycle?
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
init() is the first thing that runs in a package
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.
Package variables are initialized top to bottom
By dependencies: at each step — the earliest in declaration order among the ready ones. var total = price*qty, declared first, was initialized third.
Packages are initialized in the order of imports in the file
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.
The package name is the last element of the import path
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.
internal is a compiler or language rule
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.
A Go project must have cmd/ and pkg/
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 _.
undefined: pkg.name means the package has no such name
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.
init can be called by hand — say, in a test
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
| Version | Change | What it means for code |
|---|---|---|
| go1.13 and earlier | Per 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 specification | It 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.1 | The 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.27 | Not 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
// 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
Knowledge check
Package shape declares a function area with a lowercase letter. What happens if shape.area(1) is called from another package?
This is neither a retelling nor a separate text: everything below is taken from the article itself — its own summary, the section headings, the “actually” column and the version table. Which is why these theses cannot drift from the article.
The gist
- A package is a directory of source files with one and the same name in the
packageline; 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 itspackageline (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 — afterpriceandqty.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 runalphais initialized beforezeta, althoughmainimportszetafirst. - 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/andpkg/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 oneinitthat registers the decoder — without itimage.Decodeanswersimage: unknown format. And a program with a singlefmtis dozens of packages, all of them initialized beforemain.
In fact
- First all of the package's variables are initialized, in all files, and only then are the
init()functions called. In the runinit()sawtotal = 30— a value computed before it. - By dependencies: at each step — the earliest in declaration order among the ready ones.
var total = price*qty, declared first, was initialized third. - Dependencies first, independent ones by import path.
mainimportszetafirst, and in the runalphawas initialized earlier. The order ofimportlines means nothing. - That is a convention. The name is whatever stands in the
packageline: the directorynaming/strutilholdspackage textkit, and the referencestrutil.Shoutgivesundefined: strutil. - The specification does not have it. The import is forbidden by the go command — the function
disallowInternalin its package loader — with the messageuse of internal package ... not allowed. A package named on the command line is not stopped. - Those are conventions.
package mainintools/anywherebuilds just as incmd/greet, and the binary is named after the last element of the import path. Special meaning for the go command belongs, for example, tointernal,vendor,testdataand names starting with.and_. - Not necessarily:
shape.areaexists in the package, with a lowercase letter, and the compiler (gc go1.24.7) reportedundefined. The messagenot exported by packageappears only if the name got into the export data — likeperimeter, called by the inlinablePerimeter. - It cannot:
undefined: init. By the specification the identifierinitis not declared, and the functioninitcannot be referred to from anywhere in a program.
By version
- go1.13 and earlier
- Per 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 importsc,b,aand now expects the outputa b c.< - go1.17 specification
- It says only that imported packages are initialized before the importing one, and once. There is no "by import path" rule (
doc/go1.17_spec.htmlin the go1.24.7 distribution).< - 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.< - go1.25.1
- The compiler adds a hint to the
undefinedmessage:undefined: shape.perimeter (but have Perimeter)(when inlining is forbidden:-gcflags=all=-lor//go:noinline; a normal build still givesname perimeter not exported),undefined: shape.ωmega (but have Ωmega). The build graph of a program with a singlefmthas five more packages than on go1.24.7.< - Go 1.26 and 1.27
- Not 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.<
What is covered
- What is really being asked
- Base: a package is a directory of files with one name
- Mechanism 1: the import path and the package name are different things
- Mechanism 2: export is decided by the first letter — and only by it
- Mechanism 3: `internal/` is a go command rule, the rest are conventions
- Mechanism 4: initialization order — by dependencies, not by the text
- Mechanism 5: the blank import for the sake of `init` — and why cycles are forbidden
- Deeper: who lines packages up, how many there are in the build, and where two messages come from
- How to answer in an interview
- Next they ask
- Common misconceptions
- Version history
- Practice
- Knowledge check
Sources & further reading
6 SOURCES
- 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
- 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
- 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
- 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
- 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
- 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