The module versus package wall
You write a helper function in a new file. You try to import it in your main program. The compiler screams. You check the directory structure. You see a go.mod file you didn't write. You're tangled in the difference between the folder structure, the import path, and the version manifest. This is the classic module versus package confusion.
Go separates distribution from compilation. A module is what you version and share. A package is what you compile and import. Mixing them up leads to import errors, messy project structures, and frustration when code that looks correct refuses to build.
Concept: Shipping containers and boxes
Think of a module as a shipping container. It has a label with a version number and a manifest listing its contents. The manifest is the go.mod file. The container holds multiple boxes.
A package is one of those boxes. Inside the box, everything is packed together. All the code in a single directory with the same package declaration belongs to that package. You can't ship half a box, but you can ship a container with many boxes.
Modules manage dependencies and versions. Packages organize code into reusable units with controlled visibility. A module can contain many packages. A package always belongs to exactly one module.
Modules ship code. Packages compile code.
Minimal example
Here's the smallest module: a go.mod file and one package inside it.
# module declares the import path used to identify this collection of packages.
module example.com/myapp
# go specifies the minimum Go version required to build this module.
go 1.21
package main
// main is the entry point for the executable.
func main() {
// println confirms the module builds and runs without external dependencies.
println("Hello")
}
The go.mod file defines the module root. The main.go file defines a package named main. The tool reads go.mod to find the module path, then compiles the main package to create a binary.
Walkthrough: How the tool reads your project
When you run go run or go build, the tool starts by looking for a go.mod file. It walks up the directory tree until it finds one. That file marks the root of the module.
Everything under that directory tree belongs to the module. The tool reads the module path from go.mod. This path becomes the prefix for all import paths in the project.
Next, the tool looks for packages. A package is a single directory. All .go files in that directory must declare the same package name. The tool compiles all files in the directory together into one package unit.
If you have a main package, the tool links it to create an executable. If you have other packages, they become libraries that other packages can import.
The go.mod file must sit at the root of the module tree. You cannot have multiple go.mod files in one project. The tool rejects nested modules with a clear error.
One module per tree. One package per directory.
Import paths are URLs, not file paths
Import paths in Go look like URLs. They are not file system paths. You cannot use relative paths like ./utils or ../pkg. The import path is always absolute relative to the module path.
If your module is example.com/myapp and you have a package in internal/db, the import is example.com/myapp/internal/db. The tool maps this path to the file system by appending the relative part to the module root.
This design ensures imports work the same way regardless of where the project lives on your disk. It also makes the dependency graph explicit.
Try to use a relative import and the compiler rejects the build with import path cannot begin with . or ... Stick to the module path prefix.
Import paths are absolute from the module root. Never use dots.
Realistic example: Multiple packages and visibility
Real projects split code into multiple packages to control visibility and organize logic. Here's a module with a main package and an internal helper package.
# module declares the import path for the entire project.
module example.com/myapp
# go specifies the minimum Go version required to build this module.
go 1.21
// db.go lives in internal/db.
package db
// Connect opens a database connection.
// The package name is db, so the function is db.Connect.
func Connect() string {
return "connected"
}
// main.go lives in cmd/server.
package main
import "example.com/myapp/internal/db"
func main() {
// Import uses the module path plus the relative directory.
status := db.Connect()
println(status)
}
The internal directory is special. Go treats packages inside internal as private to the module. Other modules cannot import them. This is enforced by the compiler, not just a convention.
Visibility works at the package level. Public names start with a capital letter. Private names start with a lowercase letter. A private function in db cannot be called by main, even though both are in the same module. The package is the boundary.
Use internal to hide implementation details from the world. Capital letters export. Lowercase letters hide.
Pitfalls and errors
Modules and packages cause specific errors when misused. Recognizing these messages saves debugging time.
Forgetting the go.mod file stops the build immediately. The tool reports go: go.mod file not found in current directory or any parent directory. Run go mod init to create the file.
Mixing package declarations in one directory breaks compilation. You get found packages main and utils in /path/to/dir. Every file in a directory must share the same package name.
Importing a non-existent package triggers could not import example.com/myapp/internal/db (no metadata for ...). Check the module path and the directory structure.
Editing go.mod by hand is risky. Use go get to add dependencies and go mod tidy to clean up unused ones. The tool manages the file format and version constraints.
Trust gofmt. Argue logic, not formatting. Run gofmt on every save to keep imports and indentation consistent.
Let the tool manage the module file. Edit code, not manifests.
Decision: When to use modules and packages
Use a module when you need to version a collection of packages and share them with other projects. Use a package when you want to group related functions and types into a single compilation unit. Use the internal directory when you need to restrict package access to the current module only. Use a single package when your code is small enough to fit in one directory without leaking implementation details. Use multiple packages when you have distinct concerns that benefit from separate visibility scopes.
Modules manage versions. Packages manage scope.