Packages & Imports
The package declaration
Section titled “The package declaration”Every Go source file begins with a package declaration. The package name groups related files together and controls what identifiers are visible to the outside world.
package mainThe special package name main tells the Go toolchain that this package produces an executable binary. Every executable must have exactly one func main() inside package main — that is where execution begins.
package main
func main() { // execution starts here}Any other package name (for example package mathutil) produces a library that other packages can import.
Importing packages
Section titled “Importing packages”The import statement brings other packages into scope. You can import a single package or group multiple imports together in parentheses — Go style strongly prefers the grouped form whenever you need more than one:
// single importimport "fmt"
// grouped import — preferredimport ( "fmt" "strings" "strconv")The import path is a string that identifies the package. Standard library packages use short paths like "fmt", "strings", "math/rand". Third-party packages use a full module path like "github.com/user/repo/pkg".
The blank import
Section titled “The blank import”Occasionally you import a package solely for its side effects — typically to register a driver or handler during init(). In that case, use the blank identifier _ to suppress the “imported and not used” compile error:
import _ "github.com/lib/pq" // the driver's init() calls sql.Register("postgres", ...)Another common case is wiring up debug endpoints without calling anything from the package directly:
import _ "net/http/pprof" // registers /debug/pprof/ handlers on the default muxExported vs unexported identifiers
Section titled “Exported vs unexported identifiers”Go’s visibility rule is simple and enforced by the compiler: an identifier that starts with an uppercase letter is exported (visible to other packages); one that starts with a lowercase letter is unexported (package-private).
package mathutil
// Add is exported — other packages can call mathutil.Addfunc Add(a, b int) int { return a + b }
// helper is unexported — invisible outside this packagefunc helper() {}This applies to functions, types, variables, constants, struct fields, and interface methods alike.
Program initialization order
Section titled “Program initialization order”Before main runs, Go initializes the whole program in a strict, well-defined order — understanding it explains why a package-level variable can safely refer to another one, and where to put setup code that has to run before anything else:
- Imported packages first. If your package imports another, that package is fully initialized — its own variables and
initfunctions run to completion — before your package’s initialization begins. This happens recursively, following the import graph. - Package-level variables next, in dependency order. Go figures out which variables depend on which (by reading the initializer expressions) and initializes them so every dependency is ready before the variable that needs it — not necessarily in the order they’re written in the file.
initfunctions last, in the order they appear. A single file can declare more than onefunc init(), and a package can spreadinitfunctions across multiple files; they all run, in the order the compiler encounters them (file order, then declaration order within a file), after every package-level variable is initialized.mainruns last of all, and only inpackage main.
flowchart TB
subgraph Imported["Each imported package (recursively, dependency order)"]
IV["its package-level vars"] --> II["its init() funcs"]
end
Imported --> V["this package's vars, in dependency order"]
V --> I["this package's init() funcs, in file/declaration order"]
I --> Main["main()"] var base = 10var derived = base * 2 // base is guaranteed initialized first
func init() { fmt.Println("init #1: derived =", derived)}
func init() { fmt.Println("init #2: a file can have more than one init()")}Import aliases and dot imports
Section titled “Import aliases and dot imports”An import can be given a local name, which is useful when two imported packages would otherwise share the same default name, or when you simply want a shorter one:
import ( f "fmt")
f.Println("aliased import")A dot import — import . "strings" — pulls every exported identifier from that package directly into the current file’s scope, so you call ToUpper(...) instead of strings.ToUpper(...). It compiles and runs fine, but it is almost never used in production code: it makes it impossible to tell, just by reading a call site, which package an identifier came from, and it silently risks colliding with names already in scope. The one place it shows up legitimately is in some test files, to make table-driven assertions read closer to plain English.
internal/ packages
Section titled “internal/ packages”Any package whose import path contains a directory literally named internal is only importable by code inside the module rooted at the parent of that internal directory. This is enforced by the compiler, not a convention:
main.go:5:2: use of internal package internaldemo/internal/secret not allowedThat real error appears the moment code outside the internaldemo module tries to import internaldemo/internal/secret — even though the package is otherwise a normal, exported-identifier-having Go package. Code inside the same module tree can import it freely. Library authors use internal/ to share code between their own packages without it becoming part of their public API.
Package name vs. import path
Section titled “Package name vs. import path”The name you use to refer to a package in code is its package name — the identifier after the package keyword in its source files — which is not always the last element of its import path. math/rand/v2 is the clearest example: its import path ends in /v2 (a major version suffix, part of Go’s module versioning convention for breaking changes), but every file in it still says package rand, so you write rand.IntN(...), not v2.IntN(...):
import "math/rand/v2"
rand.IntN(100) // the package is called rand, not v2A few standard library packages have a newer, generics-based sibling worth knowing about when reading code of any age:
| Older package | Newer replacement | Why |
|---|---|---|
math/rand | math/rand/v2 | Better default algorithm, cleaner API (IntN instead of Intn), no need to seed for good randomness |
sort | slices (Go 1.21) | Generic slices.Sort, slices.SortFunc — no sort.Interface boilerplate |
Circular imports
Section titled “Circular imports”Go’s import graph must be acyclic: if package a imports package b, package b cannot import package a, directly or through any chain of other packages. The compiler rejects it outright:
package circdemo/a imports circdemo/b from a.go imports circdemo/a from b.go: import cycle not allowedThe standard fix is to break the cycle by extracting the shared pieces both packages need into a third, lower-level package that both of the originals import — or, if the dependency is only in one direction conceptually, to have the higher-level package define an interface that the lower-level one implements, removing the need for the lower-level package to import anything from the higher one at all.
go.mod in one paragraph
Section titled “go.mod in one paragraph”Every module has a go.mod file at its root, declaring the module’s import path (module example.com/myapp), its dependencies with pinned versions, and a go directive (go 1.27) that records the minimum language version the module requires. That go directive is more than documentation: the compiler reads it to decide which language semantics apply to that module’s own code — most notably, it is what gates the Go 1.22 per-iteration loop-variable change discussed in Functions, so code in a module still declaring go 1.21 keeps the pre-1.22 shared-loop-variable behavior, even when built with a much newer Go toolchain.
package main
import ( "fmt" "strconv" "strings")
func main() { // fmt: formatted output fmt.Println("Go packages demo")
// strings: manipulating text sentence := "the quick brown fox" words := strings.Split(sentence, " ") fmt.Println("Word count:", len(words)) fmt.Println("Uppercase:", strings.ToUpper(sentence)) fmt.Println("Contains 'fox':", strings.Contains(sentence, "fox"))
// strconv: converting between strings and numbers n := 42 s := strconv.Itoa(n) fmt.Println("int to string:", s)
parsed, err := strconv.Atoi("123") if err == nil { fmt.Println("string to int:", parsed) }
// Combining packages together parts := []string{"Go", strconv.Itoa(1), strconv.Itoa(22)} fmt.Println("Joined:", strings.Join(parts, "."))} The second playground demonstrates initialization order, an import alias, a dot import, and a package whose name differs from its import path.
package main
import ( f "fmt" . "strings" "math/rand/v2")
// Package-level variables initialize first, in dependency order.var base = 10var derived = base * 2 // depends on base, so base is guaranteed ready first
func init() { f.Println("init #1: derived =", derived)}
func init() { f.Println("init #2: a file can have more than one init()")}
func main() { f.Println("main: base =", base, "derived =", derived)
// f is an import alias for fmt. f.Println("aliased import works:", ToUpper("dot import too")) // ToUpper comes from the dot-imported strings package
// math/rand/v2's import path ends in /v2, but its package name is rand. f.Println("rand.IntN(1) =", rand.IntN(1))} The standard library
Section titled “The standard library”Go ships with a rich standard library. A few packages you will use constantly:
| Package | Purpose |
|---|---|
fmt | Formatted I/O: Println, Printf, Sprintf, Errorf |
strings | String manipulation: Contains, Split, ToUpper, TrimSpace |
strconv | String ↔ number conversions: Itoa, Atoi, FormatFloat |
errors | Error creation: errors.New, errors.Is, errors.As |
sort | Sorting slices and custom types |
math | Math functions: math.Abs, math.Sqrt, math.Pi |
time | Time values, durations, and formatting |
os | OS interface: file I/O, environment variables, command-line args |
Trade-offs
Section titled “Trade-offs”| What you get | Benefit | Cost |
|---|---|---|
| Uppercase = exported | Visibility rule is simple — no public/private keyword needed | You must rename an identifier to change its visibility |
| Unused import = compile error | No dead imports lingering in the codebase | You must remove every unused import (goimports helps) |
init() function | Setup happens automatically, in a well-defined order | Hidden side effects that are hard to debug and test |
internal/ packages | Share code across your own packages without exposing it publicly | Anything under internal/ cannot be imported by other modules, even if you later want it to be |
Module system (go.mod) | Reproducible builds, explicit dependencies, per-module language semantics | Steeper learning curve than the old GOPATH model |
Common Misconceptions
Section titled “Common Misconceptions”- “Circular imports work in test files.” Go does not allow circular imports, whether in production code or test files.
- “
import _only works with database drivers.” It works with any package whoseinit()you want to run — for example, registering a codec, plugin, or HTTP handler. - “A Go module is just a package manager like npm.” A Go module manages versioning and reproducible builds at the whole-module level, not just for a single package.
- “A package’s name always matches the last segment of its import path.”
math/rand/v2ispackage rand; the/v2is a version suffix in the path, not part of the name. - “
internal/is just a naming convention, like a leading underscore.” It is enforced by the compiler — importing an internal package from outside its module tree is a compile error, not a lint warning.
💡 Real-world example
database/sqluses a blank import so a driver such asgithub.com/lib/pqcan register itself viainit(), which callssql.Register("postgres", ...).
net/http/pprofuses the same mechanism —import _ "net/http/pprof"registers debug handlers under/debug/pprof/viainit(), without calling anything from the package directly.
Reading AI-generated code
Section titled “Reading AI-generated code”Generated code that declares packages, imports, or init functions tends to hide these issues:
- Does an
init()function do something that would be clearer, safer, or more testable as an explicit function called frommain? Not everyinit()earns its implicitness. - Does a package-level variable’s initializer call a function with side effects (a network call, reading a file)? That runs before
maineven starts, with no way to handle an error from it. - Is a dot import (
import . "pkg") used outside of a test file? It hides where identifiers come from and risks silent name collisions. - Does an import path ending in
/v2(or higher) get referred to asv2.Somethingin code, instead of the package’s real name? - Does the code try to import something under an
internal/directory from outside that module? That is a compile error, not a runtime surprise — but generated code sometimes assumes internal helpers are reusable across projects.
🐛 Spot the bug
import "math/rand/v2"
func main() { fmt.Println(v2.IntN(100))}Show the answer
The import path math/rand/v2 ends in /v2, but the package’s actual name — declared with package rand in its own source — is rand, not v2. The real compiler error is undefined: v2. The fix is to refer to it by its real name: rand.IntN(100). The /v2 suffix in the import path is Go’s module-versioning convention for a breaking change to the same logical package; it is not part of the identifier you use in code.