Skip to content

Packages & Imports

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 main

The 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.

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 import
import "fmt"
// grouped import — preferred
import (
"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".

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 mux

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.Add
func Add(a, b int) int { return a + b }
// helper is unexported — invisible outside this package
func helper() {}

This applies to functions, types, variables, constants, struct fields, and interface methods alike.

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:

  1. Imported packages first. If your package imports another, that package is fully initialized — its own variables and init functions run to completion — before your package’s initialization begins. This happens recursively, following the import graph.
  2. 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.
  3. init functions last, in the order they appear. A single file can declare more than one func init(), and a package can spread init functions 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.
  4. main runs last of all, and only in package 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()"]
Initialization order: imported packages, then this package's vars, then its init funcs, then main
var base = 10
var 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()")
}

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.

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 allowed

That 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.

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 v2

A few standard library packages have a newer, generics-based sibling worth knowing about when reading code of any age:

Older packageNewer replacementWhy
math/randmath/rand/v2Better default algorithm, cleaner API (IntN instead of Intn), no need to seed for good randomness
sortslices (Go 1.21)Generic slices.Sort, slices.SortFunc — no sort.Interface boilerplate

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 allowed

The 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.

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 = 10
var 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))
}

Go ships with a rich standard library. A few packages you will use constantly:

PackagePurpose
fmtFormatted I/O: Println, Printf, Sprintf, Errorf
stringsString manipulation: Contains, Split, ToUpper, TrimSpace
strconvString ↔ number conversions: Itoa, Atoi, FormatFloat
errorsError creation: errors.New, errors.Is, errors.As
sortSorting slices and custom types
mathMath functions: math.Abs, math.Sqrt, math.Pi
timeTime values, durations, and formatting
osOS interface: file I/O, environment variables, command-line args
What you getBenefitCost
Uppercase = exportedVisibility rule is simple — no public/private keyword neededYou must rename an identifier to change its visibility
Unused import = compile errorNo dead imports lingering in the codebaseYou must remove every unused import (goimports helps)
init() functionSetup happens automatically, in a well-defined orderHidden side effects that are hard to debug and test
internal/ packagesShare code across your own packages without exposing it publiclyAnything 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 semanticsSteeper learning curve than the old GOPATH model
  • “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 whose init() 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/v2 is package rand; the /v2 is 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/sql uses a blank import so a driver such as github.com/lib/pq can register itself via init(), which calls sql.Register("postgres", ...).

net/http/pprof uses the same mechanism — import _ "net/http/pprof" registers debug handlers under /debug/pprof/ via init(), without calling anything from the package directly.

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 from main? Not every init() 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 main even 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 as v2.Something in 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.

Which package name marks a Go file as producing an executable binary?
What does an identifier starting with a lowercase letter mean in Go?
In what order does Go initialize a program before main runs?
Which of the following is the idiomatic Go style when importing multiple packages?
Does this compile?
import "math/rand/v2"

func main() {
    fmt.Println(v2.IntN(100))
}
A package outside module internaldemo tries import "internaldemo/internal/secret". What happens?