Packages and Modules
Understand Go's package system and module management. Learn how to organize code, manage dependencies, create reusable packages, and structure larger Go projects.
Programs grow beyond single files. You need to organize code into logical units, reuse functionality across projects, and control what's visible to other code. Go's package system provides this organization. A package is a collection of source files in the same directory, compiled together, with controlled visibility.
Package Basics
Every Go source file starts with a package declaration:
package main
import "fmt"
func main() {
fmt.Println("Hello")
}
The package main declaration identifies this file as part of the main package. All files in the same directory must declare the same package name.
The main package is special—it produces an executable. When you build or run code in the main package, Go looks for a func main() with no parameters and no return values. Execution starts there.
All other package names create libraries. They export functions, types, and variables that other packages import:
package math
func Add(a, b int) int {
return a + b
}
Other code imports and uses it:
package main
import "myproject/math"
func main() {
result := math.Add(5, 3)
println(result)
}
Package Names and Directories
Packages map to directories. A directory contains one package, and all .go files in that directory must declare the same package name:
myproject/
main.go (package main)
math/
add.go (package math)
multiply.go (package math)
database/
postgres.go (package database)
migrations.go (package database)
The package name typically matches the directory name, but it doesn't have to. By convention, keep them the same—it reduces confusion.
Import paths reference packages by their directory location relative to the module root:
import "myproject/math"
import "myproject/database"
The import path is the directory path. The package name is what you use in code:
import "myproject/math"
result := math.Add(5, 3) // Use package name, not import path
Multiple Files in a Package
Split large packages across multiple files. All files in the directory are part of the same package:
// math/add.go
package math
func Add(a, b int) int {
return a + b
}
// math/multiply.go
package math
func Multiply(a, b int) int {
return a * b
}
// math/helpers.go
package math
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
Functions, types, and variables are visible across all files in the package. The abs function in helpers.go is accessible from add.go and multiply.go without importing anything—they're all part of the same package.
Visibility Rules
Capitalization determines visibility. Identifiers starting with uppercase letters are exported (visible outside the package). Lowercase identifiers are private (visible only within the package):
package user
// User is exported - other packages can see it
type User struct {
ID int // Exported field
Name string // Exported field
password string // Private field
}
// NewUser is exported - constructor function
func NewUser(id int, name string) *User {
return &User{
ID: id,
Name: name,
}
}
// validate is private - only code in package user can call it
func validate(u *User) error {
if u.Name == "" {
return errors.New("name required")
}
return nil
}
Other packages can use User, ID, and Name, but not password or validate:
package main
import "myproject/user"
func main() {
u := user.NewUser(1, "Alice")
println(u.Name) // OK: Name is exported
// println(u.password) // Error: password is private
}
This simple rule—capitalization—controls all visibility. No public, private, or protected keywords. Look at the identifier and you know its scope.
Standard Library Imports
The Go standard library provides packages for common tasks:
import (
"fmt" // Formatted I/O
"strings" // String manipulation
"time" // Time handling
"os" // Operating system functionality
"io" // I/O primitives
"net/http" // HTTP client and server
)
Standard library imports use short names without domains. Third-party packages use full URLs:
import (
"fmt" // Standard library
"github.com/google/uuid" // Third-party
"golang.org/x/sync/errgroup" // Go extended packages
)
Import Declarations
Group imports in a single declaration:
import (
"fmt"
"os"
"github.com/google/uuid"
"github.com/gorilla/mux"
"myproject/database"
"myproject/user"
)
By convention, separate standard library imports, third-party imports, and local imports with blank lines. This grouping is visual—it helps identify dependencies at a glance.
The goimports tool maintains this automatically. It adds missing imports, removes unused ones, and groups them correctly.
Import Aliases
Sometimes package names conflict or are too long. Use aliases:
import (
"crypto/rand"
"math/rand" // Conflict: both are called rand
)
// Use alias
import (
"crypto/rand"
mathrand "math/rand"
)
func main() {
cryptoBytes := make([]byte, 16)
rand.Read(cryptoBytes) // crypto/rand
n := mathrand.Intn(100) // math/rand
}
Aliases also shorten long package names:
import (
pb "myproject/generated/protobuf/v1/users"
)
user := &pb.User{
Name: "Alice",
}
Use aliases sparingly. They make imports harder to trace. Prefer clear package names that don't need aliases.
Blank Imports
Import a package for its side effects without using its exported names:
import _ "github.com/lib/pq"
The blank import executes the package's init functions but doesn't make its exports available. This is common with database drivers:
import (
"database/sql"
_ "github.com/lib/pq" // Registers postgres driver
)
db, err := sql.Open("postgres", connString)
The pq package registers itself with database/sql in its init function. You don't call its functions directly, but it needs to run.
Init Functions
Packages can define init functions that run when the package is initialized:
package config
var Settings Config
func init() {
// Load configuration on package initialization
data, err := os.ReadFile("config.json")
if err != nil {
log.Fatal(err)
}
json.Unmarshal(data, &Settings)
}
Init functions have no parameters and no return values. A package can have multiple init functions across multiple files. They run in the order they appear, after all variable initializations.
Init functions are useful for setup that must happen before the package is used:
package database
var db *sql.DB
func init() {
var err error
db, err = sql.Open("postgres", os.Getenv("DATABASE_URL"))
if err != nil {
log.Fatal(err)
}
}
func Query(sql string) (*sql.Rows, error) {
return db.Query(sql)
}
But init functions have downsides. They make testing harder (they run before tests), hide initialization logic, and can't return errors gracefully. Prefer explicit initialization:
package database
type DB struct {
conn *sql.DB
}
func New(dsn string) (*DB, error) {
conn, err := sql.Open("postgres", dsn)
if err != nil {
return nil, err
}
return &DB{conn: conn}, nil
}
This approach is testable and allows error handling. Use init only when you truly need automatic initialization.
Go Modules
Modern Go uses modules to manage dependencies. A module is a collection of packages with a go.mod file at the root:
myproject/
go.mod
go.sum
main.go
user/
user.go
database/
db.go
Initialize a module:
go mod init github.com/username/myproject
This creates go.mod:
module github.com/username/myproject
go 1.23
The module path identifies your module. It doesn't need to match a real repository until you publish. It's a namespace.
Adding Dependencies
Import external packages in your code:
package main
import (
"fmt"
"github.com/google/uuid"
)
func main() {
id := uuid.New()
fmt.Println(id)
}
Run any go command (go build, go run, go test) and Go downloads the dependency:
go run main.go
# go: downloading github.com/google/uuid v1.6.0
The go.mod file updates automatically:
module github.com/username/myproject
go 1.23
require github.com/google/uuid v1.6.0
The go.sum file records checksums for every dependency and their dependencies:
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
These checksums ensure reproducible builds. If someone tampers with a dependency, the checksum won't match.
Version Selection
Go modules use semantic versioning: v1.2.3 means version 1, minor version 2, patch 3.
When you import a package, Go downloads the latest compatible version. If multiple packages require different versions of the same dependency, Go's minimal version selection picks the oldest version that satisfies all requirements.
This differs from npm or pip, which prefer the newest version. Go's approach prioritizes stability—you get the minimum necessary version, not bleeding edge.
Upgrade dependencies explicitly:
go get github.com/google/[email protected]
go get -u ./... # Update all dependencies
The go get command updates go.mod and downloads new versions.
Internal Packages
The internal directory enforces visibility boundaries. Packages inside internal are only importable by code in the parent tree:
myproject/
go.mod
main.go
internal/
helpers/
helpers.go
user/
user.go
Code in myproject can import myproject/internal/helpers. Code outside myproject cannot—even if the package is public on GitHub.
This prevents external code from depending on internal implementation details:
// Inside myproject
import "myproject/internal/helpers" // OK
// Outside myproject, in another module
import "github.com/username/myproject/internal/helpers" // Error
Use internal for packages you might refactor or remove. They're visible within your module but not to external consumers.
Package Organization Patterns
Flat structure for small projects:
myproject/
go.mod
main.go
user.go
order.go
database.go
Everything in one package. Simple, minimal indirection. Works well for projects under a few thousand lines.
Domain-based packages for larger projects:
myproject/
go.mod
cmd/
server/
main.go
internal/
user/
user.go
repository.go
order/
order.go
service.go
database/
postgres.go
Each domain concept gets its own package. The cmd directory holds executables. The internal directory holds library code.
Don't organize by layer:
// Bad
myproject/
models/
controllers/
services/
repositories/
This creates dependencies in all directions. Models depend on nothing, but services depend on models and repositories, controllers depend on services. Changes ripple across packages.
Organize by domain instead. Put everything related to users in the user package:
package user
type User struct { ... } // Model
type Repository interface { ... } // Repository
type Service struct { ... } // Service
func (s *Service) Create(...) error { ... } // Logic
Dependencies flow inward—from specific to general. The user package might import database, but database doesn't import user.
Cyclic Dependencies
Go forbids circular imports. If package A imports package B, then B cannot import A:
// package a
import "myproject/b"
// package b
import "myproject/a" // Error: import cycle
This restriction forces cleaner architecture. Cyclic dependencies indicate poor separation of concerns.
Break cycles by extracting shared code into a third package:
Before:
user/ imports order/
order/ imports user/
After:
user/ imports common/
order/ imports common/
common/ (shared types)
Or rethink the dependency direction. Often one package should be more fundamental:
user/ (fundamental)
order/ imports user/
Users exist independently. Orders reference users. The dependency flows one direction.
Executable Programs
Executable programs live in the main package. For projects with multiple executables, use the cmd directory:
myproject/
go.mod
cmd/
server/
main.go (package main)
worker/
main.go (package main)
migrate/
main.go (package main)
internal/
user/
order/
Each subdirectory in cmd produces a separate binary. Build them:
go build ./cmd/server
go build ./cmd/worker
go build ./cmd/migrate
Keep main.go small. It should initialize dependencies and start the program. Business logic lives in internal packages.
Vendor Directory
The vendor directory holds dependency source code:
go mod vendor
This copies all dependencies into vendor/. Builds use vendored code instead of downloading:
go build -mod=vendor
Vendoring is optional. Use it when:
- You want to guarantee dependencies don't disappear
- You're deploying to environments without internet access
- You want faster CI builds (no download step)
Most projects don't vendor. The module proxy (proxy.golang.org) caches dependencies, making downloads fast and reliable.
What's Next
Packages organize code into manageable units with controlled visibility. Modules manage dependencies and versions. The internal directory enforces boundaries, and flat structures work until projects grow complex.
The next step is testing. Go's testing package makes tests look like regular code. You write test functions, the test runner finds them, and table-driven tests scale to hundreds of cases. Understanding how to test Go code effectively is essential for building reliable systems.
After testing, we'll tour the standard library—the packages you'll use constantly. io.Reader and io.Writer abstract I/O, encoding/json handles JSON, net/http serves and consumes HTTP, and time manages temporal data. These packages form the foundation of most Go programs.
Ready to explore testing and how Go makes testing feel like regular code?
