Packages and Modules
Lesson 11: Packages and Modules
Real-World Analogy: Imagine a large library. Each book is placed on different shelves by category — Literature, Science, History... This is a "package." The library's index system tells you where each shelf is — this is a "module." You don't need to pile all books in one room; categorizing them is how you efficiently find what you need. Go's package system is the library classification of the code world.
Core Concepts
| Concept | Description |
|---|---|
| Package | The organizational unit of Go source code; all .go files in a directory belong to the same package |
| Module | A collection of related packages, defined by the go.mod file; the smallest unit of dependency management |
| Import | Use exported identifiers from other packages |
| Export Rules | Identifiers starting with an uppercase letter are accessible from external packages; lowercase means package-private |
| internal package | Special directory name; internal packages can only be imported by code in their parent directory tree |
| go get | Download and install third-party packages from remote repositories |
Relationship Between Packages and Modules
my-module/ ← Module root, contains go.mod
├── go.mod
├── main.go ← package main
├── mathutil/ ← Sub-package mathutil
│ └── calc.go ← package mathutil
└── internal/ ← Internal package
└── secret.go ← package internal
Basic Syntax and Usage
1. package Declaration
Every Go source file's first line must be a package declaration:
package main // Entry package for executable programs
package mathutil // Utility package
.go files in the same directory must declare the same package name (usually matching the directory name).
2. Initializing a Module
# Execute in the project root directory
go mod init my-module
# Generates go.mod file:
# module my-module
#
# go 1.24
github.com/username/my-module, making it easy for others to reference.
3. Tidying Dependencies
# Add missing dependencies, remove unused ones
go mod tidy
go mod tidy every time you introduce a new third-party package.
4. import Statements
import "fmt" // Standard library
import "github.com/gin-gonic/gin" // Third-party package
// Grouped imports (recommended)
import (
"fmt"
"log"
"os"
)
5. Export Rules
package mathutil
var Pi = 3.14159 // Starts with uppercase → exported, externally visible
var version = "1.0" // Starts with lowercase → unexported, package-private only
func Add(a, b int) int { // Uppercase → exported
return a + b
}
func subtract(a, b int) int { // Lowercase → unexported
return a - b
}
6. internal Package
myapp/
├── internal/
│ └── auth/ ← Only myapp/ and its subdirectories can import
│ └── auth.go
└── cmd/
└── server/
└── main.go ← ✅ Can import myapp/internal/auth
internal is compiler-enforced access control, not a convention — any package outside its parent directory tree cannot import it.
7. Installing Third-Party Packages
# Download package and update go.mod / go.sum
go get github.com/gin-gonic/gin
# Specify version
go get github.com/gin-gonic/gin@v1.9.1
# Remove unused dependencies
go mod tidy
Examples
Example: Creating and Using Custom Packages (Difficulty ⭐)
Project structure:
greet-app/
├── go.mod
├── main.go
└── greeting/
└── greet.go
greeting/greet.go:
package greeting
import "fmt"
// Hello returns a greeting (uppercase, exported)
func Hello(name string) string {
return fmt.Sprintf("Hello, %s! Welcome to Go programming.", name)
}
// farewell returns a farewell message (lowercase, unexported)
func farewell(name string) string {
return fmt.Sprintf("Goodbye, %s!", name)
}
// Goodbye is an exported wrapper for farewell (uppercase, exported)
func Goodbye(name string) string {
return farewell(name)
}
main.go:
package main
import (
"fmt"
"greet-app/greeting" // Import local sub-package
)
func main() {
// Use exported functions
fmt.Println(greeting.Hello("Alice"))
fmt.Println(greeting.Goodbye("Alice"))
// ❌ The following code will error: farewell is unexported
// fmt.Println(greeting.farewell("Alice"))
}
Run:
go run main.go
Output:
Hello, Alice! Welcome to Go programming.
Goodbye, Alice!
Example: Using Third-Party Packages (Difficulty ⭐⭐)
Initialize project:
mkdir http-demo && cd http-demo
go mod init http-demo
go get github.com/gin-gonic/gin
main.go:
package main
import (
"net/http"
"github.com/gin-gonic/gin" // Third-party web framework
)
// User defines a user struct
type User struct {
Name string `json:"name"`
Email string `json:"email"`
}
func main() {
r := gin.Default() // Create default engine
// GET request: return welcome message
r.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "Welcome to the Go package management tutorial!",
})
})
// GET request: return user info
r.GET("/user", func(c *gin.Context) {
user := User{
Name: "Alice",
Email: "alice@example.com",
}
c.JSON(http.StatusOK, user)
})
// Start server, listen on port 8080
r.Run(":8080")
}
Run:
go run main.go
# After server starts, visit http://localhost:8080
Example: internal Package and Multi-Package Collaboration (Difficulty ⭐⭐⭐)
Project structure:
bank-system/
├── go.mod
├── main.go
├── account/
│ └── account.go ← Account package (public)
├── internal/
│ └── validator/
│ └── validator.go ← Validator package (internal only)
└── transaction/
└── transaction.go ← Transaction package (public)
go.mod:
module bank-system
go 1.24
internal/validator/validator.go:
package validator
import "errors"
// ValidateAmount validates transaction amount (internal package only)
func ValidateAmount(amount float64) error {
if amount <= 0 {
return errors.New("amount must be greater than zero")
}
if amount > 1000000 {
return errors.New("single transaction cannot exceed 1 million")
}
return nil
}
// ValidateAccountID validates account ID
func ValidateAccountID(id string) error {
if len(id) < 6 {
return errors.New("account ID must be at least 6 characters")
}
return nil
}
account/account.go:
package account
import (
"fmt"
"bank-system/internal/validator" // Import internal package
)
// Account struct
type Account struct {
ID string
Name string
Balance float64
}
// NewAccount creates a new account
func NewAccount(id, name string, balance float64) (*Account, error) {
// Use internal package's validation function
if err := validator.ValidateAccountID(id); err != nil {
return nil, fmt.Errorf("account creation failed: %w", err)
}
if balance < 0 {
return nil, fmt.Errorf("initial balance cannot be negative")
}
return &Account{
ID: id,
Name: name,
Balance: balance,
}, nil
}
// Deposit deposits money
func (a *Account) Deposit(amount float64) error {
// Use internal package's validation function
if err := validator.ValidateAmount(amount); err != nil {
return fmt.Errorf("deposit failed: %w", err)
}
a.Balance += amount
return nil
}
// GetBalance gets the balance
func (a *Account) GetBalance() float64 {
return a.Balance
}
transaction/transaction.go:
package transaction
import (
"fmt"
"bank-system/account"
"bank-system/internal/validator" // Can also import internal package
)
// Transfer transfer function
func Transfer(from, to *account.Account, amount float64) error {
// Use internal package to validate amount
if err := validator.ValidateAmount(amount); err != nil {
return fmt.Errorf("transfer failed: %w", err)
}
if from.GetBalance() < amount {
return fmt.Errorf("transfer failed: insufficient balance (current: %.2f)", from.GetBalance())
}
// Execute transfer
if err := from.Deposit(-amount); err != nil {
return fmt.Errorf("deduction failed: %w", err)
}
if err := to.Deposit(amount); err != nil {
// Rollback
from.Deposit(amount)
return fmt.Errorf("credit failed: %w", err)
}
return nil
}
main.go:
package main
import (
"fmt"
"bank-system/account"
"bank-system/transaction"
// ❌ The following import will fail: internal package cannot be imported externally
// "bank-system/internal/validator"
)
func main() {
// Create two accounts
acc1, err := account.NewAccount("ACC001", "Alice", 10000)
if err != nil {
fmt.Println("Error:", err)
return
}
acc2, err := account.NewAccount("ACC002", "Bob", 5000)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("Before transfer - Alice: %.2f, Bob: %.2f\n", acc1.GetBalance(), acc2.GetBalance())
// Execute transfer
err = transaction.Transfer(acc1, acc2, 3000)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("After transfer - Alice: %.2f, Bob: %.2f\n", acc1.GetBalance(), acc2.GetBalance())
// Test invalid amount
err = acc1.Deposit(-100)
if err != nil {
fmt.Println("Expected error:", err)
}
}
Run:
go run main.go
Output:
Before transfer - Alice: 10000.00, Bob: 5000.00
After transfer - Alice: 7000.00, Bob: 8000.00
Expected error: deposit failed: amount must be greater than zero
Real-World Application Scenarios
Scenario 1: Building a Reusable Logger Package
my-app/
├── go.mod
├── main.go
└── logger/
└── logger.go
logger/logger.go:
package logger
import (
"fmt"
"os"
"time"
)
// Level log level
type Level int
const (
LevelDebug Level = iota // 0
LevelInfo // 1
LevelWarn // 2
LevelError // 3
)
// Logger struct
type Logger struct {
prefix string
level Level
}
// New creates a logger instance
func New(prefix string, level Level) *Logger {
return &Logger{
prefix: prefix,
level: level,
}
}
// log internal method for outputting logs
func (l *Logger) log(level Level, tag, msg string) {
if level < l.level {
return
}
timestamp := time.Now().Format("2006-01-02 15:04:05")
line := fmt.Sprintf("[%s] [%s] [%s] %s\n", timestamp, tag, l.prefix, msg)
os.Stdout.WriteString(line)
}
// Debug debug log
func (l *Logger) Debug(msg string) {
l.log(LevelDebug, "DEBUG", msg)
}
// Info info log
func (l *Logger) Info(msg string) {
l.log(LevelInfo, "INFO", msg)
}
// Warn warning log
func (l *Logger) Warn(msg string) {
l.log(LevelWarn, "WARN", msg)
}
// Error error log
func (l *Logger) Error(msg string) {
l.log(LevelError, "ERROR", msg)
}
main.go:
package main
import (
"my-app/logger"
)
func main() {
log := logger.New("APP", logger.LevelInfo)
log.Debug("This won't show because level is below Info")
log.Info("Application started")
log.Warn("Disk space low")
log.Error("Database connection failed")
}
Output:
[2026-06-26 10:30:00] [INFO] [APP] Application started
[2026-06-26 10:30:00] [WARN] [APP] Disk space low
[2026-06-26 10:30:00] [ERROR] [APP] Database connection failed
Scenario 2: Package Organization in Layered Architecture
A typical web service project structure:
web-service/
├── go.mod
├── go.sum
├── main.go
├── config/
│ └── config.go ← Configuration management
├── internal/
│ ├── model/
│ │ └── user.go ← Data model
│ ├── repository/
│ │ └── user_repo.go ← Data access layer
│ └── service/
│ └── user_service.go ← Business logic layer
└── handler/
└── user_handler.go ← HTTP handler layer
config/config.go:
package config
// Config application configuration
type Config struct {
Port string
DBHost string
DBPort int
LogLevel string
}
// Load loads configuration (simplified example)
func Load() *Config {
return &Config{
Port: "8080",
DBHost: "localhost",
DBPort: 5432,
LogLevel: "info",
}
}
internal/model/user.go:
package model
// User model
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
internal/repository/user_repo.go:
package repository
import (
"fmt"
"web-service/internal/model"
)
// UserRepository user data repository
type UserRepository struct {
users map[int]*model.User
nextID int
}
// NewUserRepository creates a repository instance
func NewUserRepository() *UserRepository {
return &UserRepository{
users: make(map[int]*model.User),
nextID: 1,
}
}
// Create creates a user
func (r *UserRepository) Create(name, email string) *model.User {
user := &model.User{
ID: r.nextID,
Name: name,
Email: email,
}
r.users[r.nextID] = user
r.nextID++
return user
}
// FindByID finds a user by ID
func (r *UserRepository) FindByID(id int) (*model.User, bool) {
user, ok := r.users[id]
return user, ok
}
// FindAll finds all users
func (r *UserRepository) FindAll() []*model.User {
result := make([]*model.User, 0, len(r.users))
for _, u := range r.users {
result = append(result, u)
}
return result
}
// String returns a summary of the repository
func (r *UserRepository) String() string {
return fmt.Sprintf("UserRepository{%d users total}", len(r.users))
}
main.go:
package main
import (
"fmt"
"web-service/config"
"web-service/internal/repository"
)
func main() {
// Load config
cfg := config.Load()
fmt.Println("Port:", cfg.Port)
// Use repository
repo := repository.NewUserRepository()
repo.Create("Alice", "alice@example.com")
repo.Create("Bob", "bob@example.com")
fmt.Println(repo)
// Find all users
for _, u := range repo.FindAll() {
fmt.Printf(" ID=%d, Name=%s, Email=%s\n", u.ID, u.Name, u.Email)
}
}
Output:
Port: 8080
UserRepository{2 users total}
ID=1, Name=Alice, Email=alice@example.com
ID=2, Name=Bob, Email=bob@example.com
❓ FAQ
Q1: Do package names and directory names have to match?
Not enforced, but strongly recommended. Go conventions specify that package names should match directory names. They can differ, but it causes confusion:
// myutil/calc.go
package calculator // Package name doesn't match directory name
// Import uses the path, but usage uses the package name
import "my-module/myutil" // Path
calculator.Add(1, 2) // Uses package name
Q2: Can I use anything for go mod init module path?
Yes, but using a repository path is recommended:
# ✅ Recommended: easy for others to reference
go mod init github.com/yourname/yourproject
# ⚠️ Also fine: local projects
go mod init myproject
# ❌ Avoid: special characters or spaces
go mod init "my project"
Q3: Why is the go.sum file needed?
go.sum records the cryptographic hash of each dependency, ensuring downloaded code hasn't been tampered with. It should be committed to version control along with go.mod:
# go.mod — declares dependencies and versions
# go.sum — verifies dependency integrity
# Both are essential
git add go.mod go.sum
Q4: How do I organize multiple files in the same package?
All .go files in the same directory share the same package and can directly reference each other without importing:
// mathutil/add.go
package mathutil
func Add(a, b int) int { return a + b }
// mathutil/multiply.go — same package, can directly use Add
package mathutil
func Multiply(a, b int) int {
result := 0
for i := 0; i < b; i++ {
result = Add(result, a) // Direct call, no import needed
}
return result
}
📖 Summary
| Key Point | Content |
|---|---|
| package declaration | Every .go file must declare its package name on the first line |
| Module initialization | go mod init <module-path> creates go.mod |
| Dependency management | go mod tidy tidies dependencies, go get installs third-party packages |
| Export rules | Uppercase = exported, lowercase = package-private only |
| internal package | Compiler-enforced access control, only importable by parent directory tree |
| Best practices | Package name matches directory name, organize by responsibility, avoid circular dependencies |
📝 Exercises
Exercise 1: Create a String Utility Package
Create a stringutil package with the following exported functions:
Reverse(s string) string— Reverse a stringToUpper(s string) string— Convert to uppercaseWordCount(s string) int— Count words
Then import and use it in main.go.
Exercise 2: internal Package Practice
Create a project with an internal/config package for reading configuration (as a struct), and an app package that uses the config. Verify: the app package can import internal/config, but external packages cannot.
Exercise 3: Multi-Package Calculator
Design a calculator project with the following packages:
calc/operation— Basic operations (add, subtract, multiply, divide)calc/scientific— Scientific operations (power, factorial)internal/validator— Input validation (division by zero checks, negative number checks, etc.)
Requirements: scientific package can call operation package functions, and all validation logic goes in internal/validator.



