Packages & Modules
A package is how Go groups related code. A module is how Go groups related packages into a single shippable unit with versioned dependencies. You touched both in Chapter 4; this chapter goes deep.
Learning objectives
- Organize code into packages and import them.
- Export names via capitalization, and understand why.
- Use
initfunctions correctly (and sparingly). - Create a module with
go mod init, add dependencies, and understandgo.sum. - Know the role of
internal/andvendor/.
Packages
A package is a directory of .go files that all share the
same package X declaration at the top. There are two
kinds:
package main: produces an executable.- Any other name: produces a library, importable by others.
Typical project layout:
myapp/
├── go.mod
├── go.sum
├── main.go ← package main
├── cmd/
│ └── tool/
│ └── main.go ← second binary
├── internal/
│ └── store/
│ └── store.go ← package store (internal)
└── pkg/
└── util/
└── util.go ← package util (public)
Exports, capitalization is the rule
A name (function, type, variable, constant, method, field) is exported (visible outside the package) iff its first letter is uppercase:
package mathutil
func Add(a, b int) int { return a + b } // exported
func sub(a, b int) int { return a - b } // unexported
From another package: mathutil.Add(1, 2) works,
mathutil.sub(1, 2) is a compile error.
main and init
main() is the program's entry point, in
package main only.
init() runs automatically when a package is loaded,
before main. Use it to initialize package-level
state or register things:
package store
import "database/sql"
import _ "github.com/lib/pq" // side-effect import: registers the driver
var db *sql.DB
func init() {
// runs once, before any exported function is called
db = openDB()
}
Multiple files in a package can each have their own init;
they all run (in file name order). Use init sparingly;
implicit magic makes tests harder and dependencies muddier.
Imports
import (
"fmt"
"strings"
"github.com/user/project/internal/store"
. "math" // dot import, makes names accessible unqualified (avoid)
log "log/slog" // alias import
_ "image/png" // side-effect only, registers format
)
Go groups imports into standard library first, then third party,
separated by a blank line. goimports (Chapter 4) does this
for you automatically.
Modules, go.mod
A module is declared by a go.mod file at its root:
module github.com/cyrus2281/learn-go
go 1.22
require (
github.com/lib/pq v1.10.9
golang.org/x/text v0.14.0
)
Four things are encoded:
- module path: the canonical import path consumers use.
- go version: minimum Go version.
- require: direct dependencies and their versions.
- go.sum (sibling file), cryptographic hashes of every dependency for reproducibility.
Adding a dependency
go get github.com/lib/pq@latest # adds/upgrades to latest
go mod tidy # remove unused, fill in missing
Or just import and run go build: Go will fetch the
dependency automatically and update go.mod.
To upgrade one dep:
go get github.com/lib/pq@v1.11.0
Semantic versioning
Go modules require semver-style tags: v1.2.3.
- v0.x: "anything goes", used for early development.
- v1.x: stable API; minor/patch bumps don't break.
- v2+: major version bump changes the module path:
github.com/user/lib/v2. This lets different major versions coexist.
internal/ and vendor/
internal/ is magic: packages under an
internal/ directory can only be imported by code in the
module that contains it. Great for hiding implementation details even
in an open-source project.
vendor/ stores copies of your dependencies
in the repo. go mod vendor creates it. Uncommon in modern
Go, module proxy caching makes it mostly unnecessary. Still used when
you need reproducible builds without network access.
Workspaces (go work)
When you're working on multiple modules at once (e.g. a library and
an app that uses it, both in local directories), a workspace lets Go
resolve imports to your local copies without replace
directives:
go work init ./myapp ./mylib
# creates go.work at the parent level
A go.work file lives outside version control, your
workspace choice is personal.
Check your understanding
Practice exercises
Split a hello world into two packages
Take the Hello World from Chapter 4. Move the greeting logic into a new package greet (in a greet/ subdirectory) with an exported Hello(name string) string function. Import and call it from main.
Show solution
myapp/
├── go.mod (module example.com/myapp)
├── main.go
└── greet/
└── greet.go
greet/greet.go:
package greet
import "fmt"
func Hello(name string) string {
return fmt.Sprintf("Hello, %s!", name)
}
main.go:
package main
import (
"fmt"
"example.com/myapp/greet"
)
func main() {
fmt.Println(greet.Hello("World"))
}
Further reading
- Managing dependencies (official)
- Go Modules reference
- The Go Blog, Organizing Go code
- Standard project layout (community, not official, use judiciously)
Code organized and shippable.