Packages and Modules
How Go code is organised, why a capital letter is the only access modifier you get, and what go.mod actually does.
Every Go file starts by declaring its package:
package mainA package is a directory. Every .go file in that directory must declare the same package name, and together they form one unit — files in the same package can use each other's identifiers with no import.
package main is special: it produces an executable, and needs a func main().
Exported means capitalised#
Go's only access control is the case of the first letter:
package pricing
const TaxRate = 0.2 // exported — usable as pricing.TaxRate
const baseMargin = 0.15 // unexported — package-private
func Total(items []Item) int { } // exported
func applyMargin(n int) int { } // unexported
type Item struct {
SKU string // exported field
costCents int // unexported field
}That is the whole mechanism. No public, no private. It applies to constants, variables, functions, types, methods and struct fields alike.
The consequence worth knowing early: an unexported field is invisible to encoding/json, so a struct field you forgot to capitalise silently disappears from your API responses.
Modules#
A module is a collection of packages versioned together, defined by go.mod at its root:
go mod init github.com/you/myappmodule github.com/you/myapp
go 1.23
require (
github.com/google/uuid v1.6.0
golang.org/x/sync v0.8.0
)The module path is the import prefix for everything inside it. A package in internal/pricing/ is imported as github.com/you/myapp/internal/pricing.
go get github.com/google/uuid # add a dependency
go mod tidy # add what is used, remove what is not
go mod why github.com/x/y # why is this in my graph?go.sum records a cryptographic hash of every module version, verified on every build. Commit both files.
Imports#
import (
"fmt" // standard library
"net/http"
"github.com/google/uuid" // third party
"github.com/you/myapp/internal/pricing" // your own
)gofmt groups and sorts these. Two forms worth knowing:
import mrand "math/rand" // alias, to disambiguate
import _ "github.com/lib/pq" // blank — for side effects only (driver registration)An unused import is a compile error, not a warning. That is a small thing that keeps generated Go tidy in a way other languages are not.
The internal directory#
A package under a directory named internal can only be imported by code within the same module subtree:
github.com/you/myapp/
internal/pricing/ <- importable only inside myapp
pkg/client/ <- importable by anyoneThis is enforced by the compiler and is the right default: put everything in internal/ unless you intend other projects to import it. Once something is publicly importable you own its API.
A standard layout#
myapp/
go.mod
go.sum
cmd/
server/main.go # one directory per binary
worker/main.go
internal/
pricing/ # domain logic
pricing.go
pricing_test.go
storage/ # database access
httpapi/ # handlers
pkg/ # only if genuinely reusable by otherscmd/ holds thin main packages that wire things together; the real code lives in internal/. For a small tool, a single main.go at the root is perfectly fine — do not build this structure before you need it.
init and package state#
var registry = make(map[string]Handler)
func init() {
registry["default"] = defaultHandler
}init() runs once, after package-level variables are initialised, before main. A package can have several, and they run in file order.
Use it rarely. Initialisation order across packages is hard to reason about, init cannot return an error, and it makes testing awkward. An explicit constructor called from main is almost always better.
Naming#
Go's naming conventions are unusually strong, and following them makes code look native:
- Package names are short, lowercase, single words:
http,pricing,strconv. No underscores, no camelCase, no plurals. - Do not stutter. In package
pricing, name the functionTotal, notPricingTotal— callers writepricing.Total. - Short names for short scopes.
i,r,w,errare idiomatic. Long descriptive names are for package-level identifiers. - Interfaces that hold one method end in
-er:Reader,Writer,Stringer.
Exercise#
// Sketch the layout for a URL-shortener module `github.com/you/shortly`:
// - a binary at cmd/shortly
// - domain logic (creating and resolving short codes) that other repos must NOT import
// - a storage package, also private
// - one exported type and one unexported helper in the domain package
// Write the go.mod, and the package + import lines for each file.Common questions#
internal/ or pkg/?#
internal/ by default. It is compiler-enforced privacy, and it means you can refactor freely because nothing outside your module can depend on it. Move something to pkg/ only when you have decided to support it as a public API.
Why is an unused import an error rather than a warning?#
Deliberate strictness — it keeps builds clean and stops dead imports accumulating. It is occasionally annoying while debugging, which is what the blank identifier _ is for as a temporary measure.
Should I use init()?#
Rarely. It cannot return an error, its ordering across packages is subtle, and it makes tests harder to isolate. Prefer an explicit New... constructor called from main, where failures can be handled.
Get the Go agent pack
A battle-tested AGENTS.md, the review checklist, and the failure-mode cheat sheet for Go. One email, then occasional updates when the tooling shifts. No course pitch.
AGENTS.md now — no email needed.