Packages, Imports and Initialization
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Go 1.27 (go1.27.2), checked on 2026-10-18. Every runnable program below was vetted with go vet,
compiled and run with that toolchain, and the output (including compiler errors) is the real output. A package is Go’s unit of
code organization, compilation and encapsulation: a directory of .go files that share a package name, import other packages by path,
and expose a public API through capitalized identifiers. This page explains how packages map to directories, the import forms, why import
cycles are forbidden, how internal/ enforces boundaries, in which order package-level variables and init functions run, and how to lay out
a real module such as shelf. The module-level concepts (versions, go.mod, dependencies) are on
Modules and Dependency Management; the identifier and scope rules are on
Program Structure and Lexical Elements.
Packages and directories
Every Go source file begins with a package clause. All the files of one package live in one directory, and a directory holds exactly one package (plus, optionally, its external test package, described below). The compiler rejects a directory whose files disagree:
x/a.go: package catalog
x/b.go: package store
$ go build ./x
found packages catalog (a.go) and store (b.go) in /work/shelf/x (path shortened)
The rules in brief:
-
A package is the set of
.gofiles in one directory with the same package name. Files are compiled together, and every package-level identifier declared in any of them is visible in all of the others, without any import. -
The package name is the name used by importers to qualify identifiers (
catalog.Book). The import path is a string that locates the package ("example.com/shelf/catalog"). By convention the last element of the path is the package name, but the language does not require it, which is the source of one of the pitfalls shown later. -
The package name
mainis special: amainpackage with afunc main()is built into an executable (a command). A package namedmaincannot be imported by another package:m2.go:2:8: import "example.com/shelf/m" is a program, not an importable package -
Files whose names end in
_test.gobelong to the package under test, or to an external test package whose name is the package name plus_test(for examplepackage catalog_test). The external package may live in the same directory and imports the package under test like any other client, so it can only use its exported API. See Testing.
Build constraints (//go:build) choose which files of a directory take part in a build; see The go Command, Builds and Cross-Compilation.
The shelf layout
The running shelf example has one library package, one helper package that must stay private, and two commands. The tool can print the
real import edges with go list:
go list -f '{{.ImportPath}} ({{.Name}}) -> {{join .Imports " "}}' ./...
example.com/shelf/catalog (catalog) -> errors fmt strings sync
example.com/shelf/cmd/shelf (main) -> example.com/shelf/catalog example.com/shelf/internal/textutil fmt
example.com/shelf/cmd/shelfd (main) -> encoding/json example.com/shelf/catalog log/slog net/http
example.com/shelf/internal/textutil (textutil) -> strings
package main"] srv["cmd/shelfd
package main"] cat["catalog
public API: Book, ISBN, Store"] subgraph int["internal/ (importable only inside the module)"] tu["internal/textutil
Normalize"] end end cli --> cat cli --> tu srv --> cat cat -.-> std1["errors, fmt, strings, sync"] srv -.-> std2["encoding/json, log/slog, net/http"] tu -.-> std3["strings"] ext["other modules"] -. "may import" .-> cat ext -. "compile error" .-> tu
Solid edges are imports between packages of the module, dotted edges are imports of the standard library, and the last two edges show what other modules may and may not import. Edges only ever point from commands towards libraries and from libraries towards helpers: the graph has no cycle, and that is a requirement, not a style choice.
Import paths and import declarations
An import declaration names the packages whose exported identifiers the file uses. It is file-scoped: each file lists its own imports, and an import in one file is not visible in the others, even in the same package.
package main
import (
"fmt"
"math"
str "strings"
"example.com/shelf/greet"
_ "example.com/shelf/greet"
. "unicode/utf8"
)
func main() {
fmt.Println(str.ToUpper(greet.Hello("shelf")), math.Sqrt(16), RuneCountInString("héllo"))
}
greet: init
HELLO, SHELF 4 5
The greet package here has an init function that prints greet: init. It ran once, although the package was imported twice (a
duplicate import of the same path is legal, but pointless). The three non-default forms are:
| Form | Effect | Typical use |
|---|---|---|
|
Binds the package to the local name |
Resolving a name clash between two imports; renaming a package whose name differs from the last path element; shortening a long name |
|
Imports the package only for its side effects: it is initialized (its |
Registering database drivers and image decoders; see the registration example below |
|
Makes the package’s exported identifiers visible in the file without a qualifier ( |
Almost never; it hides where a name comes from. A few external test packages use it deliberately |
Rules worth knowing:
-
The import path is a string literal. For the standard library it is a short path (
"net/http"); for everything else it starts with the module path fromgo.mod("example.com/shelf/catalog"). Relative paths such as"./catalog"are not allowed in module mode. -
The qualifier is the package name declared in the imported package’s clause, not necessarily the last path element.
math/rand/v2is used asrand; a package atexample.com/shelf/ythat declarespackage libis used aslib, and the following program does not compile:package main import "example.com/shelf/y" func main() { y.Run() }./z.go:3:14: undefined: yImport with an explicit alias (
y "example.com/shelf/y") or, better, name the package after its directory. -
An imported package that is never used is a compile error, which keeps dependency lists honest:
cmd/a/main.go:5:2: "os" imported and not used -
Referring to an unexported identifier of another package is also an error, at the qualified use and not at the import:
cmd/a/main.go:11:42: undefined: greet.secret -
gofmtsorts imports inside blank-line separated blocks (conventionally standard library first, then the rest);goimportsalso adds and removes them. See Formatting, vet, fix and Static Analysis. -
import "C"is not an ordinary import; it turns on cgo (cgo and C Interoperability). -
A local variable named like an imported package (
url,path) shadows the import inside its scope, a classic cause of confusing errors.
Import cycles
Package A cannot import package B if B (directly or through other packages) imports A. The compiler needs packages to be built bottom-up,
and Go chose to forbid cycles rather than support them. If shelf tried to have catalog ask a loans package whether a book is lent while
loans uses catalog.Book, the go command stops before compiling anything:
package example.com/shelf/catalog
imports example.com/shelf/loans from catalog.go
imports example.com/shelf/catalog from loans.go: import cycle not allowed
The error lists the cycle in order, so it is easy to read. A cycle is a design signal: two packages that need each other are usually one package, or share a concept that belongs in a third one. The usual repairs are:
-
Merge the two packages, when they are really one cohesive unit.
-
Extract the shared types into a lower-level package that both import (for example, move
Bookintocatalogand haveloansdepend on it, whilecatalogdepends on nothing inloans). -
Invert the dependency with an interface.
catalogdeclares the small interface it needs (type LoanChecker interface { Active(isbn ISBN) bool }) andloansprovides a type that satisfies it, with no import ofcatalogin the lender’s direction. Interfaces are satisfied implicitly, so the lower package never has to name the higher one; see Interfaces. -
Pass a function value instead of importing the package that supplies it, for hooks and callbacks.
Cycles are detected between packages, not between files. An external test package catalog_test may import loans even when loans imports catalog, because nothing imports catalog_test.
Exported and unexported identifiers
Go has no public or private keyword. An identifier is exported when its first character is an upper-case Unicode letter and it is declared
at package level (or is a field or method name). Everything else is visible only inside its own package. This applies uniformly to types,
functions, variables, constants, struct fields and methods:
package greet
func Hello(name string) string { return "hello, " + name } // exported
func secret() string { return "x" } // unexported: usable only inside package greet
Consequences:
-
The unit of encapsulation is the package, not the type. Two types in one package can read each other’s unexported fields.
-
Exported struct fields are visible to
encoding/jsonand other reflection-based packages; unexported ones are ignored (Structs and Embedding). -
Renaming an exported identifier breaks every importer; keep the exported surface small.
Internal packages
Some packages are public inside a module but should never become part of its API. The go command enforces one rule for that: a package path
that contains the path element internal can be imported only by code rooted at the parent of the internal directory. For shelf,
example.com/shelf/internal/textutil can be imported by anything below example.com/shelf/, and by nobody outside it. A second module that
tries gets this error from the go command, not from the compiler, so it appears before any compilation:
package example.com/app
main.go:6:2: use of internal package example.com/lib/internal/secret not allowed
Source: go command: Internal packages
The rule applies at any depth: a/b/internal/c is importable from a/b and below, not from a. The standard library uses it heavily (crypto/internal/boring). internal lets a library change helpers without a major version bump.
Package initialization
A program starts by initializing its packages and then calling main.main. The language specifies the whole process, and the result is
deterministic for a given set of sources.
-
Packages are initialized one at a time. A package is initialized only after all the packages it imports are initialized, so a dependency graph (the one shown above) is walked leaves first. A package imported by several others is initialized once.
-
Inside a package, package-level variables are initialized first, and then the
initfunctions run. -
For variables, the rule is dependency analysis: repeatedly select the earliest variable in declaration order that is ready, meaning it has no initialization expression, or its expression refers to no uninitialized variable of the package, directly or through the functions and methods it calls. The declaration order is the order in which files are presented to the compiler (the go tool sorts them by file name), and within a file the textual order.
-
Each
initfunction then runs in the order it appears in the files presented to the compiler. A file may contain severalinitfunctions, and one package may have many files withinitfunctions. -
Finally,
main.mainruns.
The following program contains every piece of that: a main package whose variables depend on each other in a different order than they are declared,
two init functions, and two imported packages that print as they initialize.
package main
import (
"fmt"
"example.com/shelf/initb"
)
var (
a = c + b // depends on c and b
b = f() // depends on d
c = f() // depends on d
d = 3
)
func f() int {
d++
fmt.Println("f called, d =", d)
return d
}
func init() { fmt.Println("main: init 1") }
func main() {
fmt.Println("main: a, b, c, d =", a, b, c, d, "initb.X =", initb.X)
}
func init() { fmt.Println("main: init 2") }
Package initb imports inita; inita has two files, a.go and b.go, with a variable initialized by a function that prints, and
three init functions (one in a.go, two in b.go). The output:
var inita.B
inita: init in a.go
inita: init in b.go (1)
inita: init in b.go (2)
initb: init, X = 2
f called, d = 4
f called, d = 5
main: init 1
main: init 2
main: a, b, c, d = 9 4 5 5 initb.X = 2
Reading the output: inita finished before initb because initb imports it; inside inita the variable initializer ran before any init although
it is declared in the second file, and a.go came before b.go by file name. In main, a is declared first but depends on b and c, which depend
on d, so the order is d, b, c, a (hence b is 4, c is 5 and a is 9). Both init functions ran after all variables and before main.
The dependency analysis is purely textual: it follows references to variables and calls to functions and methods, but not calls through interface values or function-valued variables, which the compiler cannot resolve. An initialization that has a reference cycle is rejected:
package main
import "fmt"
var total = count()
func count() int { return limit + 1 }
var limit = total * 2
func main() { fmt.Println(total, limit) }
cyc/c.go:5:5: initialization cycle for total
cyc/c.go:5:5: total refers to count
cyc/c.go:7:6: count refers to limit
cyc/c.go:9:5: limit refers to total
The error shows the complete chain, including the hidden step through the function count. Package-level initialization cycles are also
reported by go vet, whose output differs only in the prefix.
The init function
func init() is a function with no parameters and no results that the runtime runs automatically. It can appear any number of times in a
package, including several times in one file. It is not a declared identifier: it cannot be called, referenced or assigned.
package main
func init() {}
func main() { init() }
./init_ref.go:5:15: undefined: init
Source: Effective Go: The init function
An init function is legitimate for work that cannot be expressed as a variable initializer, such as validating or repairing program state before
main runs, or registering a type in a registry. Its costs are the reason most style guides advise against it:
-
It runs for every program and test binary that imports the package, even when none of its code is used, so slow work makes every import slow.
-
It cannot return an error; the only reactions are to panic or log, which callers cannot recover from or test.
-
It hides dependencies and encourages mutable package-level state, so reading
maindoes not explain what the program does and tests interfere with each other.
The preferred alternatives are an explicit constructor that returns an error (catalog.New(opts)), a variable initializer that calls a pure
function, and lazy initialization on first use. The standard library gives the last one a one-line form: sync.OnceValue computes a value the first time it is needed and
caches it. See Sync and Atomics for Once, OnceFunc, OnceValue and OnceValues.
Source: sync: func OnceValue
Registration and blank imports
The one widespread, legitimate use of init is registration: a package registers an implementation in a registry owned by another
package, and the program selects it by importing the implementation for its side effects. database/sql drivers, image decoders and crypto hashes work
this way. The program below decodes a one-pixel PNG without importing a PNG decoder:
package main
import (
"bytes"
"encoding/base64"
"fmt"
"image"
// _ "image/png" // uncomment: the blank import runs png's init, which registers the decoder
)
const tinyPNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
func main() {
raw, _ := base64.StdEncoding.DecodeString(tinyPNG)
cfg, format, err := image.DecodeConfig(bytes.NewReader(raw))
fmt.Println(cfg.Width, cfg.Height, format, err)
}
0 0 image: unknown format
With the import line _ "image/png" enabled, the same program prints:
1 1 png <nil>
Source: image package
The same pattern with a database driver reads import _ "github.com/lib/pq" in the program’s main package, followed by
sql.Open("postgres", dsn); this is shown here as an illustration of the form only, since it needs a third-party module and a server (the
database/sql page has the complete flow, see database/sql). Two points matter:
-
Put the blank import in the
mainpackage (or a small package imported only bymain), not in a library, so that the choice of implementation belongs to the program and not to a dependency. -
Keep the registering
inittrivial (one call to aRegisterfunction). A driver whoseinitopens connections is a bug.
Order between unrelated packages
The language leaves the order of unrelated packages to the toolchain. Since Go 1.21 the specification describes it: list all packages sorted by import
path and repeatedly initialize the first one whose imports are all initialized. Code must rely on the import graph only, never on that tie-break.
There is no "deinit": use defer in main, signal.NotifyContext and explicit Close methods.
Organizing a module
The official guidance lists a few shapes and lets the module grow from one to the next: a basic package (go.mod plus .go files in the
root, the import path is the module path); a basic command (package main in the root); a package or command with supporting packages under
internal/; multiple packages in top-level directories; multiple commands under cmd/<name>/; and a server project, where cmd/ holds the
commands and internal/ almost everything else, because nothing outside the module should import the code.
Source: Organizing a Go module
shelf is a multiple-commands layout (cmd/shelf, cmd/shelfd) whose shared code is the catalog package plus internal/textutil, as the diagram above shows, so a user can run
go install example.com/shelf/cmd/shelf@latest and import example.com/shelf/catalog from their own programs.
Guidelines that follow from the language rules and from the official layout page:
-
Start small. One package in one directory is a complete, idiomatic module. Split when a clear boundary appears, not in advance; a premature
pkg/,models/,controllers/andutils/skeleton recreates the cycles and the grab-bag packages that Go avoids. -
Group by what the code provides, not by its kind.
catalogis a good package;models,util,commonorhelperscollect unrelated code and become the dependency hub. -
Give each command its own
cmd/<name>/directory and keepmainthin (flags, wiring, a call into library code); see Command-Line Programs. -
Use
internal/by default for what is not API, and promote a package out when you commit to supporting it. -
The module root is the directory with
go.mod; a nested directory with its owngo.modis a different module (Workspaces, Versioning and Publishing). -
The popular
golang-standards/project-layoutrepository (/pkg,/internal,/cmd,/api,/web…) is a community convention. It is not an official standard, the layout page above is the reference. Thepkg/directory in particular adds a path element without adding meaning.
Package naming
A package name is part of every call site (catalog.NewMemStore), so it carries meaning that the exported names need not repeat. The Go
blog’s package-naming article and the Code Review Comments page converge on a short list:
-
Short, lower case, no underscores or mixed case:
catalog,textutil,httputil. NotCatalog,text_utilortextUtil. -
Singular nouns named after what they provide (
catalog, notcatalogs,typesormodels). -
Avoid stutter. Inside
catalog, usecatalog.Storeandcatalog.NewMemStore, notcatalog.CatalogStore. -
Match the last element of the import path, with the exception of major-version suffixes: the package at
example.com/shelf/v2/catalogis stillcatalog, andmath/rand/v2declarespackage rand. -
Do not shadow standard packages or common variables (
url,string,time), and avoid generic suffixes such asutils. Test packages append_test.
Source: The Go Blog: Package names
Source: The Go Blog: Organizing Go code
Source: Go Wiki: Go Code Review Comments
Where the books are outdated
-
GOPATH and remote packages. Vivien (ch. 1 and ch. 6) teaches packages from the GOPATH workspace, with code under
$GOPATH/srcandgo getas the way to fetch and build remote packages, and Bodner (ch. 1 and ch. 9) still discusses the GOPATH-era relationship between repositories and packages while presenting modules. Since Go 1.16 module mode is the default, an import path is resolved throughgo.mod, andgo getonly edits dependencies (installing a program isgo install path@version). The package and import rules on this page are the same in both worlds, but the directory a package comes from is now determined by the module, not by a workspace layout. -
Initialization order. Vivien (ch. 6) describes initialization as the spec did at the time. Go 1.21 made the algorithm precise, including the sorted-by-import-path tie-break between unrelated packages.
-
Vendoring. A
vendor/directory is part of module mode (go mod vendor,-mod=vendor), see Modules and Dependency Management.
Version notes
| Release | Change relevant to this page |
|---|---|
Go 1.4 / 1.5 |
The |
Go 1.11 / 1.16 |
Modules introduced (1.11) and enabled by default (1.16); GOPATH mode is legacy |
Go 1.21 |
Package initialization order specified precisely (sorted by import path, dependency-driven) |
Source: Go 1.21 release notes