On this page

Modules and Packages

12 min read TextCh. 5 — Go in Production

Go's Module and Package System

Go's module system, introduced in Go 1.11 and made the standard in Go 1.16, solves one of the most complicated problems in any language: reproducible, versioned dependency management. Before modules, Go used GOPATH — a system that caused many problems with dependency versions.

Understanding the difference between modules and packages is fundamental:

  • Package: a directory with .go files sharing the same package name. It is the unit of code organization.
  • Module: a collection of packages with a common import prefix and a go.mod file. It is the unit of distribution and versioning.

Initializing a Module

# Create the project directory
mkdir my-api
cd my-api

# Initialize the module with the import path
go mod init github.com/yourusername/my-api

# Result: go.mod file created
cat go.mod
module github.com/yourusername/my-api

go 1.26

The module path is the unique identifier for your module. By convention use the repository path (GitHub, GitLab, etc.), but it can be any string.

Idiomatic Project Structure

my-api/
├── go.mod
├── go.sum
├── main.go                  ← entry point
├── cmd/
│   ├── server/
│   │   └── main.go         ← HTTP server
│   └── migrate/
│       └── main.go         ← migration CLI tool
├── internal/                ← private packages (not importable outside the module)
│   ├── config/
│   │   └── config.go
│   ├── handler/
│   │   ├── user.go
│   │   └── user_test.go
│   └── repository/
│       └── user.go
├── pkg/                     ← public packages (importable by other modules)
│   └── validator/
│       └── validator.go
└── api/
    └── openapi.yaml

This structure follows the Standard Go Project Layout, though for small projects it is perfectly acceptable to have everything in the root directory.

Packages: The Unit of Organization

All .go files in the same directory must declare the same package name:

// user/user.go
package user

type User struct {
    ID   int
    Name string
}

// user/repository.go
package user  // same directory, same package

type Repository struct {
    db *sql.DB
}

Exported vs. Unexported Identifiers

In Go, visibility is controlled by the case of the first character:

package mypackage

// EXPORTED — visible from outside the package
type User struct {
    ID    int    // exported field
    Name  string // exported field
    email string // unexported field — lowercase
}

// EXPORTED
func NewUser(name, email string) *User {
    return &User{Name: name, email: email}
}

// EXPORTED
const MaxUsers = 1000

// EXPORTED
var ErrNotFound = errors.New("user not found")

// UNEXPORTED — only visible in this package
func validateEmail(email string) bool {
    return strings.Contains(email, "@")
}

// UNEXPORTED
type internalConfig struct {
    timeout int
    retries int
}

Importing Packages

// Single import
import "fmt"
import "os"
import "net/http"

// Block import (preferred)
import (
    "fmt"
    "os"
    "net/http"
    "time"

    // External dependencies — separate group by convention
    "github.com/gin-gonic/gin"
    "gorm.io/gorm"
)

// Alias to avoid name collisions
import (
    mathRand   "math/rand"
    cryptoRand "crypto/rand"
)

// Side-effect import (only init())
import _ "github.com/lib/pq"  // register PostgreSQL driver

// Dot import — import all into current scope (rare)
import . "fmt"  // allows using Println instead of fmt.Println

The `internal` Directory

Go has a special rule: packages inside an internal directory can only be imported by code within the parent directory tree:

my-app/
├── internal/
│   └── config/       ← only importable from within my-app/
│       └── config.go
└── cmd/
    └── server/
        └── main.go   ← can import my-app/internal/config

This lets you have internal APIs without exposing them publicly — essential for large projects.

Dependency Management

Adding a Dependency

# Add the latest version
go get github.com/gin-gonic/gin

# Add a specific version
go get github.com/gin-gonic/[email protected]

# Add the latest pre-release
go get github.com/gin-gonic/gin@latest

# Update all dependencies to latest patch/minor
go get -u ./...

# Only update patches (safer)
go get -u=patch ./...

Removing a Dependency

# After removing the imports from code:
go mod tidy

The `go.mod` File

module github.com/yourusername/my-api

go 1.26

require (
    github.com/gin-gonic/gin v1.10.0
    github.com/go-playground/validator/v10 v10.22.0
    gorm.io/driver/postgres v1.5.9
    gorm.io/gorm v1.25.12
)

require (
    // indirect dependencies (managed automatically)
    golang.org/x/crypto v0.26.0 // indirect
    ...
)

The `go.sum` File

go.sum contains cryptographic hashes of each dependency. Go verifies these checksums before using any module, guaranteeing dependency integrity. Never edit go.sum manually — Go manages it automatically.

Essential Module Commands

# View all dependencies (direct and indirect)
go list -m all

# See why a dependency is included
go mod why github.com/gin-gonic/gin

# Verify dependency integrity
go mod verify

# Download all dependencies to the local cache
go mod download

# Create a vendor/ directory (for reproducible builds without network)
go mod vendor

# Clean the module cache
go clean -modcache

Installing Executables with `go install`

# Install a Go executable
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest

# The binary is installed in $GOPATH/bin (or $GOBIN)
# Make sure $GOPATH/bin is in your PATH
which golangci-lint

Special Packages

`package main`: The Entry Point

Every executable Go program has exactly one package main with a main() function:

// main.go
package main

import "fmt"

func main() {
    fmt.Println("Hello, world!")
}

The `init()` Function

Each package can have one or more init() functions that run automatically when the package is loaded, before main():

package config

var globalConfig *Config

func init() {
    // This function runs automatically
    globalConfig = loadConfigFromEnv()
}

Execution order: all init() functions in dependencies → package's init()main().

Organizing Tests

Tests in Go live alongside the code they test, in _test.go files:

package/
├── user.go
└── user_test.go tests in the same directory

# Run tests
go test ./...          # all packages
go test ./internal/... # internal only
go test -v ./...       # verbose
go test -cover ./...   # with coverage

With the module system mastered, in the next lesson we will build REST APIs using Go's powerful standard library HTTP server.

The package name must match the directory name
By convention (and for tools to work correctly), the package name declared in package X must match the name of the directory containing the files. The only exception is package main, which can be in any directory and is the program entry point.
go mod tidy keeps go.mod and go.sum clean
Run go mod tidy regularly. It adds missing dependencies to go.mod and go.sum and removes ones that are no longer used. It is like npm install but smarter: it also cleans up orphaned dependencies. Always run it before committing.