Go modules and packages are the two levels of code organization in Go. A package is a directory of .go files compiled together, sharing one namespace. A module is a versioned collection of packages, defined by a go.mod file at its root, and it is the unit you download, version and depend on. Once you understand how the two fit together, imports, visibility and dependency management stop being confusing.
This guide walks through a small project from go mod init to third-party dependencies, covering package main, exported and unexported names, go mod tidy and the basics of project layout.
package main and func main
Every .go file starts with a package clause. One package name is special: package main tells the compiler to build an executable, and func main() in that package is where the program starts.
package main
import "fmt"
func main() {
fmt.Println("hello, modules")
}Any other package name produces a library that other packages import. It cannot run on its own, and go run on it fails with package ... is not a main package.
A few rules worth knowing:
- All
.gofiles in one directory must declare the same package name. Test files ending in_test.goare the one exception, since they may use<name>_test. maintakes no arguments and returns nothing. Read arguments fromos.Argsor theflagpackage, and exit with a non-zero status viaos.Exitorlog.Fatal.- Before
mainruns, Go initializes imported packages, including their package-level variables and anyinit()functions. Keepinitfor small registrations; hidden side effects there are hard to debug.
Creating a module with go mod init
Create a directory and initialize a module:
mkdir shapes && cd shapes
go mod init example.com/shapesThis writes a go.mod file:
module example.com/shapes
go 1.25.1moduleis the module path. It is the prefix of every import path inside the module. If you plan to publish the code, use the repository location, such asgithub.com/your-org/shapes, sogo getcan find it. For private or throwaway projects, any domain-like name works.gois the minimum Go version the module needs.go mod initfills in the version you have installed. Since Go 1.21 it is enforced: an older toolchain will refuse to build the module or will download a newer one, depending on theGOTOOLCHAINsetting.
Organizing code into packages
Here is a module with one library package, shape, split over three files, plus a main package at the root:
shapes/
├── go.mod
├── main.go # package main
└── shape/
├── shape.go # package shape
├── circle.go # package shape
└── rectangle.go # package shape// shape/shape.go
package shape
// Shape is anything with an area and a perimeter.
type Shape interface {
Area() float64
Perimeter() float64
}// shape/circle.go
package shape
import "math"
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.Radius }// shape/rectangle.go
package shape
import "fmt"
type Rectangle struct {
width, height float64 // unexported: only package shape can access them
}
func NewRectangle(w, h float64) (Rectangle, error) {
if w <= 0 || h <= 0 {
return Rectangle{}, fmt.Errorf("invalid rectangle %vx%v", w, h)
}
return Rectangle{width: w, height: h}, nil
}
func (r Rectangle) Area() float64 { return r.width * r.height }
func (r Rectangle) Perimeter() float64 { return 2 * (r.width + r.height) }The main package imports it with the module path plus the directory:
// main.go
package main
import (
"fmt"
"log"
"example.com/shapes/shape"
)
func main() {
rect, err := shape.NewRectangle(3, 4)
if err != nil {
log.Fatal(err)
}
for _, s := range []shape.Shape{shape.Circle{Radius: 1}, rect} {
fmt.Printf("%T area=%.2f perimeter=%.2f\n", s, s.Area(), s.Perimeter())
}
}Run it with go run ., which builds the package in the current directory. go build produces a binary, and go build ./... compiles every package in the module.
Notice that circle.go can use Shape from shape.go without importing anything. Files in the same package share one scope, so splitting a package into files is purely for readability.
Exported vs unexported names
Go has no public or private keywords. Visibility depends on the first letter of the name:
| Name | Visible from | Examples |
|---|---|---|
| Starts with an uppercase letter | Any package that imports it | Circle, NewRectangle, Area, Radius |
| Starts with a lowercase letter | Only the same package | width, height, helper functions |
The rule applies to types, functions, methods, variables, constants and struct fields. The boundary is the package, not the file: rectangle.go and circle.go can see each other's lowercase names, but main cannot:
r, _ := shape.NewRectangle(3, 4)
fmt.Println(r.width)
// compile error: r.width undefined (cannot refer to unexported field width)This is how Go does encapsulation. Rectangle keeps its fields unexported so the only way to build one is NewRectangle, which validates the input. One side effect to remember: encoding/json and other reflection-based packages ignore unexported fields, so a struct you want to serialize needs exported fields with tags. Go structs, methods and interfaces covers those mechanics in detail.
Naming packages
- Use short, lowercase, single-word names:
shape,auth,storage. No underscores or camelCase. - Name the directory the same as the package. Go allows them to differ, but readers expect them to match.
- Avoid stutter. Callers write
shape.Circle, soshape.ShapeCirclerepeats itself. - Avoid catch-all names like
util,commonorhelpers. They attract unrelated code and say nothing at the call site.
Importing third-party packages
To use a package from another module, import it and let the go command fetch it. Either add the dependency explicitly:
go get github.com/google/uuid@latestor write the import first and run go mod tidy:
import "github.com/google/uuid"
id := uuid.NewString()Either way, two files change:
go.modgains arequireline, such asrequire github.com/google/uuid v1.6.0. Dependencies you don't import directly are marked// indirect.go.sumrecords cryptographic checksums for each module version. The go command verifies downloads against it and against the public checksum database. Commit both files.
By default modules are downloaded through proxy.golang.org. For private repositories, set GOPRIVATE=github.com/your-org/* so the go command fetches them directly and skips the public checksum database.
Versions follow semantic versioning. From v2 onward, the major version is part of the import path, for example github.com/go-playground/validator/v10. This lets two major versions coexist in one build. When several dependencies need different minor versions of the same module, Go picks the lowest version that satisfies all of them (minimal version selection), so builds stay reproducible without a separate lock file.
Cleaning up with go mod tidy
Go refuses to compile a file with an unused import, but nothing stops go.mod from listing a module you no longer use. go mod tidy reconciles go.mod and go.sum with the imports in your code: it adds anything missing and removes requirements that nothing imports. Run it after adding or deleting imports, and in CI check that it produces no diff.
Other commands you will use:
| Command | What it does |
|---|---|
go get example.com/[email protected] | Add or change a dependency to a specific version |
go get -u ./... | Upgrade dependencies to newer minor and patch versions |
go get example.com/pkg@none | Remove a dependency |
go list -m all | List every module in the build |
go list -m -u all | Show which modules have updates |
go mod why example.com/pkg | Explain why a module is needed |
go mod verify | Check the module cache against go.sum |
For developer tools such as code generators, Go 1.24 added the tool directive: go get -tool golang.org/x/tools/cmd/stringer records it in go.mod, and go tool stringer runs the pinned version.
Working on two modules at once
When you change a library and an app together, point the app at your local copy. A replace directive in go.mod does it, but it is easy to commit by accident. A workspace keeps the override out of go.mod:
go work init ./app ./shapesThis creates go.work, which most teams leave out of version control.
Project layout basics
Small modules can stay flat: main.go at the root and a few packages beside it. As a project grows, two conventions help:
myservice/
├── go.mod
├── cmd/
│ ├── api/main.go # one directory per binary
│ └── worker/main.go
└── internal/
├── article/ # importable only inside myservice
└── storage/cmd/<name>/holds onepackage mainper executable, each kept thin: read config, wire dependencies, start.internal/is enforced by the compiler. Packages under it can be imported only by code rooted at the parent ofinternal, so other modules can't depend on your internals.
You don't need a pkg/ directory or a deep folder tree. Organize packages around what they do, not around technical layers. Go error handling and project layout goes further into how layout and error handling evolve together, and the Gin and GORM REST API guide shows these rules applied to a real service.
FAQ
What is the difference between a Go module and a package?
A package is one directory of code compiled together. A module is a versioned set of packages with a go.mod at its root. You import packages, and you version and download modules.
What does go mod tidy do?
It adds missing requirements for packages you import, removes requirements nothing imports, and updates go.sum to match.
Should I commit go.sum?
Yes. It makes builds verifiable and reproducible. Commit it together with go.mod.
What is the difference between go get and go install?
go get changes the dependencies in your current module's go.mod. go install example.com/cmd@latest builds and installs a binary into $GOBIN without touching go.mod.
Checklist
- One
go.modat the repository root, with a module path matching where the code lives. - Every directory is one package, named after the directory.
- Only names other packages need start with an uppercase letter.
- Dependencies are added with
go getor imports plusgo mod tidy, and bothgo.modandgo.sumare committed. - Executables live in
cmd/, and private code lives ininternal/.
Getting modules and packages right early makes every later refactor cheaper. If your team is starting a Go codebase and wants help setting up its structure, Vectorkub can help.
