Organizing Code into Packages
A Go package is the basic unit of code organization: a directory of .go files that all declare the same package name and compile together as a single unit. Packages let you split a program into focused, reusable pieces, control what other code can see through capitalization, and share code across projects using modules. Once a Go program grows past a single file, understanding how packages and modules fit together becomes essential.
What a Package Is, and How Go Organizes Code
Every Go source file begins with a package clause, such as package main or package greetings. All files in the same directory must declare the same package name — the compiler treats the directory as one compilation unit and merges every file’s top-level declarations (functions, types, variables, constants) into a single namespace. You can freely call a function defined in a.go from b.go in the same package without importing anything, because they are, as far as the compiler is concerned, one piece of code.
Packages are grouped into modules. A module is a collection of related packages that are versioned and distributed together, declared by a go.mod file at the root of a project. The go.mod file records the module’s import path (for example example.com/greetings), the Go version it targets, and any external dependencies. This module system, introduced in Go 1.11 and now the only supported workflow, replaced the older GOPATH approach where all code had to live under a single global workspace. With modules, a project can live anywhere on disk and still resolve its own dependencies correctly.
Each package has both an import path and a package name, and they are not always the same thing. The import path is the module path plus the subdirectory — example.com/greetings — while the package name is whatever the files inside declare with the package keyword, conventionally matching the last element of the directory. The standard library shows this clearly: the import path math/rand refers to a package named rand, so code that imports it refers to rand.Intn, not math_rand.Intn.
Visibility between packages is controlled entirely by capitalization — Go has no public/private keywords. An identifier (function, type, variable, constant, struct field, or method) that starts with an uppercase letter is exported and visible to any package that imports it. A lowercase identifier is unexported and only visible inside its own package. This single rule is why Go code reads so consistently: you know a name’s visibility just by looking at its first letter.
One package name is special: package main marks a package as a program entry point rather than a library. A main package must contain a function named main with no parameters and no return values — that function is where execution starts when you run go run or a compiled binary. Every other package is a library package, meant to be imported, and has no special entry-point function.
Under the hood, the Go compiler builds a dependency graph from your imports and compiles packages bottom-up: a package’s dependencies are fully compiled (and cached) before the package itself, and the compiler refuses to build anything with an unused import or an unused local variable — these are compile errors, not warnings, which keeps codebases free of dead weight. Packages can also define an init function, which runs automatically after package-level variables are initialized and before main runs; it’s useful for one-time setup but should be used sparingly, since implicit execution order across files can make code harder to follow.
Syntax
A package’s structure comes from a small set of declarations and directives:
| Form | Meaning |
|---|---|
package name |
First non-comment line of every .go file; declares which package the file belongs to. |
import "fmt" |
Imports a single package by its import path. |
import (...) |
Grouped import block — the idiomatic way to import more than one package in one statement. |
import m "math" |
Aliased import; refers to the package locally as m instead of math. |
import _ "net/http/pprof" |
Blank import — runs the package’s init functions for their side effects without using any of its exported names. |
module example.com/app |
First line of go.mod; declares the module’s import path. |
go 1.21 |
Line in go.mod declaring the minimum Go language version the module requires. |
Examples
Example 1: A Two-Package Module
The most common real-world structure is a small library package imported by a main package. Here is a minimal module with that shape:
greetings-app/
├── go.mod
├── main.go
└── greetings/
└── greetings.go
go.mod declares the module’s import path and Go version:
module example.com/greetings
go 1.21
greetings/greetings.go defines the library package. Hello starts with a capital letter, so it is exported; shout is lowercase, so it stays private to the greetings package:
// Package greetings provides simple greeting utilities.
package greetings
import "fmt"
// Hello returns a greeting for the given name.
func Hello(name string) string {
return fmt.Sprintf("Hello, %s!", name)
}
// shout is unexported; only visible inside package greetings.
func shout(s string) string {
return s + "!!!"
}
main.go imports the library by its full import path — the module path plus the subdirectory — and calls the exported Hello function:
package main
import (
"fmt"
"example.com/greetings"
)
func main() {
message := greetings.Hello("Gopher")
fmt.Println(message)
}
Output:
$ go run .
Hello, Gopher!
Notice that main.go refers to the package by its short name, greetings, taken from the package greetings clause — not by its longer import path. Also notice that shout is never accessible from main.go; trying to call greetings.shout(...) would fail to compile, because unexported identifiers cannot cross a package boundary.
Example 2: Package-Level State and init
Within a single package, top-level variables and an optional init function let you prepare state before main runs:
package main
import "fmt"
var greeting string
func init() {
greeting = "Hello from init"
}
func main() {
fmt.Println(greeting)
fmt.Println(Version)
}
// Version is an exported package-level constant.
const Version = "1.0.0"
Output:
Hello from init
1.0.0
Go initializes package-level variables first, then runs any init functions in the package, and only after that does it call main. Declaration order in the source doesn’t matter here — Version is used in main even though it’s declared below it, because the compiler resolves all package-level names before running anything.
Example 3: Aliased Imports
When an imported package’s default name is inconvenient — too long, or clashing with another import — you can give it a local alias:
package main
import (
"fmt"
m "math"
)
func main() {
radius := 4.0
area := m.Pi * m.Pow(radius, 2)
fmt.Printf("Area: %.2f\n", area)
}
Output:
Area: 50.27
The alias m only affects this file; it doesn’t rename the math package anywhere else. Aliasing is most useful for disambiguating two imported packages that would otherwise share the same default name, such as importing both a project’s own log package and the standard library’s log package in the same file.
How It Works Step by Step
When you run go build or go run, the toolchain does roughly the following:
- Reads
go.modto determine the module’s import path and required Go version, and resolves any external dependencies listed there (downloading and caching them if needed). - Builds an import graph starting from the package you asked it to build, following every
importstatement to find the full set of packages involved. - Compiles bottom-up: packages with no unbuilt dependencies compile first, and their compiled output is cached; a package is only compiled after everything it imports has already been compiled successfully.
- Type-checks and enforces cleanliness at each package: unused imports and unused local variables are compile errors, not warnings, so a stray import fails the whole build immediately rather than silently lingering.
- Links the final binary (for a
mainpackage) by combining all compiled packages into a single executable, or produces an archive (for a library package) to be reused by whatever imports it.
This is also why circular imports are rejected: if package a imports package b, and b imports a, there is no valid bottom-up compile order, and the build fails with an import cycle not allowed error before either package finishes compiling.
Common Mistakes
1. Leaving an unused import in place
Go treats an unused import as a compile error, not a warning:
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println("Hello")
}
This fails with "strings" imported and not used. Either remove the import or actually use the package:
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.ToUpper("hello"))
}
Output:
HELLO
2. Assuming lowercase names are just a style choice
Unexported identifiers are not merely a convention — the compiler actively enforces the boundary. Given the greetings package from Example 1, this fails to build:
package main
import (
"fmt"
"example.com/greetings"
)
func main() {
fmt.Println(greetings.shout("hi"))
}
The error is undefined: greetings.shout — code outside the greetings package simply cannot see shout at all, regardless of what it imports. Rename the function to Shout in the library if it needs to be callable from other packages.
3. Letting files in one directory disagree on package name
Every .go file in a directory must declare the same package name. Mixing names in the same folder is a build error:
// a.go
package geometry
// b.go, same directory — WRONG
package shapes
Go reports something like found packages geometry (a.go) and shapes (b.go). Fix it by giving both files the same package clause — pick one name for the directory and use it everywhere inside that folder.
4. Creating an import cycle
If package orders imports package customers, and customers also imports orders back, the build fails immediately with import cycle not allowed:
// package orders
package orders
import "example.com/app/customers"
type Order struct {
Customer customers.Customer
}
// package customers — creates a cycle if it imports orders back
package customers
import "example.com/app/orders"
type Customer struct {
LastOrder orders.Order
}
The usual fix is to break the cycle by extracting the shared type into a third package that both sides import, or by restructuring so the dependency only runs in one direction.
Best Practices
- Keep package names short, lowercase, and free of underscores or mixedCaps —
httpclient, nothttp_clientorHTTPClient. - Avoid “stutter”: if the package is
widget, name its main type so callers writewidget.Widget, notwidget.WidgetType, since callers already writewidget.before it. - One package per directory, and one clear responsibility per package — resist the urge to create a catch-all
utilsorcommonpackage that accumulates unrelated code. - Use an
internal/directory for code that should only be importable from within your own module — the Go toolchain enforces this restriction automatically based on the path. - Document every exported identifier with a comment starting with its own name (
// Hello returns...) —go docand most editors surface these directly. - Prefer grouped, non-aliased imports; reserve aliasing for genuine name collisions, and avoid dot imports (
import . "fmt") outside of generated code or specific test patterns, since they hide where a name comes from. - If two packages want to import each other, treat that as a signal to extract a shared package rather than force the dependency through.
Practice Exercises
- Create a module with a
stringutilpackage containing an exportedReverse(s string) stringfunction, and amainpackage that imports it and prints the reverse of"Gopher". Expected output:rehpoG. - Add an unexported helper function to
stringutilthatReversecalls internally, then try calling that helper directly frommain— confirm you get a compile error, and explain in your own words why. - Split a single-file program with three unrelated functions into two packages based on responsibility, giving each package a focused name, and update the imports in
mainaccordingly.
Summary
- A package is a directory of
.gofiles sharing onepackagedeclaration; a module is a versioned group of packages defined by ago.modfile. - Capitalization determines visibility — exported identifiers start with an uppercase letter, unexported ones are private to their package, with no separate access keywords.
package mainwith afunc main()marks a program’s entry point; every other package is a library meant to be imported.- The compiler builds packages bottom-up from the import graph, rejects unused imports and variables, and refuses circular imports outright.
- Good package design favors small, focused, clearly named packages over grab-bag “utils” packages, with
internal/reserved for implementation details.
