Go Packages

Why This Matters

In a complex web application with authentication, database operations, API handlers, business logic, and utilities, improper organization leads to thousands of lines of tangled code. This makes it impossible to find anything, test components independently, or enable effective collaboration among multiple developers.

This is exactly the chaos that Go packages solve. They provide scalable organization that keeps large codebases manageable, allows teams to work independently, and enables proper separation of concerns.

Real-World Impact:

  • Team Collaboration: Different teams own different packages
  • Code Reusability: Share packages across multiple projects and organizations
  • Testing: Test individual packages in isolation with clear boundaries
  • Maintenance: Find and fix bugs quickly when code is well-organized

Packages are the foundation of building maintainable Go applications. Master them, and you'll build systems that scale gracefully instead of becoming unmaintainable.

Learning Objectives

By the end of this article, you'll master:

  1. Package Philosophy: Understand Go's approach to code organization and modularity
  2. Visibility Control: Master the capitalization system for public/private APIs
  3. Import Management: Organize dependencies efficiently and avoid common pitfalls
  4. Package Design: Create packages that are focused, reusable, and maintainable
  5. Project Structure: Organize large applications with proven architectural patterns
  6. Best Practices: Apply industry standards for naming, documentation, and testing

Core Concepts - Understanding Go's Package Philosophy

The Go Philosophy: Simplicity Meets Power

Go's package system was designed from day one to be simple yet powerful. Unlike languages that added modules as an afterthought, Go built packages into its foundation.

Key Design Principles:

  1. One package per directory - Clear, predictable organization
  2. Explicit imports - No hidden dependencies, everything is visible
  3. Capitalization-based visibility - Elegant, no keywords needed
  4. No circular imports - Forces good architectural decisions

Why This Matters:

  • Predictable structure: Anyone can understand your project organization
  • Forces good design: Circular dependencies are prevented at compile time
  • Fast compilation: Clear dependency graph enables fast builds
  • Team scalability: Clear boundaries allow parallel development

What Is a Package, Really?

A package is a collection of Go source files in the same directory that:

  • Share the same package name
  • Are compiled together as one unit
  • Can access each other's unexported members
  • Present a unified API to external code

Visualizing Package Structure:

myproject/
├── go.mod                 # Module definition
├── main.go                # Entry point
├── utils/                 # utils package directory
│   ├── strings.go         # Part of utils package
│   ├── validation.go      # Part of utils package
│   └── conversion.go      # Part of utils package
└── auth/                  # auth package directory
    ├── auth.go            # Main auth logic
    ├── middleware.go      # HTTP middleware
    └── tokens.go          # Token handling

Key Insights:

  • All .go files in utils/ belong to utils package
  • Files within same package can access each other's private members
  • External packages only see exported members
  • Package boundary is enforced at compile time

Practical Examples - From Basic to Advanced

Example 1: Basic Package Creation

Let's start with the fundamentals - creating and using a simple utility package:

 1// run
 2package main
 3
 4import (
 5	"fmt"
 6)
 7
 8// Simulating mathutils package inline
 9func Add(a, b int) int {
10	return a + b
11}
12
13func Multiply(a, b int) int {
14	return a * b
15}
16
17func Divide(a, b int) int {
18	if b == 0 {
19		return 0
20	}
21	return a / b
22}
23
24func main() {
25	// Use functions
26	sum := Add(10, 20)
27	fmt.Printf("10 + 20 = %d\n", sum)
28
29	product := Multiply(5, 6)
30	fmt.Printf("5 × 6 = %d\n", product)
31
32	result := Divide(10, 0)
33	fmt.Printf("10 ÷ 0 = %d (safe handling)\n", result)
34
35	result2 := Divide(10, 2)
36	fmt.Printf("10 ÷ 2 = %d\n", result2)
37}

What's happening:

  1. Functions like Add, Multiply, Divide would be exported from a mathutils package
  2. Exported functions are accessible from other packages
  3. Internal validation ensures safe operations

Example 2: Package with Structs and Methods

Let's build a user package that demonstrates encapsulation and clean API design:

  1// run
  2package main
  3
  4import (
  5	"fmt"
  6	"regexp"
  7	"time"
  8)
  9
 10// User represents a user in our system
 11// Only exported fields are accessible from outside the package
 12type User struct {
 13	id        int       // Private
 14	Email     string    // Public
 15	Name      string    // Public
 16	password  string    // Private
 17	createdAt time.Time // Private
 18}
 19
 20// NewUser creates a new user with validation
 21func NewUser(email, name, password string) (*User, error) {
 22	if err := validateUserData(email, name, password); err != nil {
 23		return nil, err
 24	}
 25
 26	u := &User{
 27		id:        generateID(),
 28		Email:     email,
 29		Name:      name,
 30		password:  hashPassword(password),
 31		createdAt: time.Now(),
 32	}
 33
 34	return u, nil
 35}
 36
 37// GetEmail returns the user's email
 38func (u *User) GetEmail() string {
 39	return u.Email
 40}
 41
 42// UpdateEmail changes the user's email with validation
 43func (u *User) UpdateEmail(newEmail string) error {
 44	if !isValidEmail(newEmail) {
 45		return fmt.Errorf("invalid email format")
 46	}
 47	u.Email = newEmail
 48	return nil
 49}
 50
 51// CheckPassword verifies the password
 52func (u *User) CheckPassword(password string) bool {
 53	return hashPassword(password) == u.password
 54}
 55
 56// validateUserData validates user input (private)
 57func validateUserData(email, name, password string) error {
 58	if email == "" || name == "" || password == "" {
 59		return fmt.Errorf("all fields are required")
 60	}
 61
 62	if !isValidEmail(email) {
 63		return fmt.Errorf("invalid email format")
 64	}
 65
 66	if len(password) < 8 {
 67		return fmt.Errorf("password must be at least 8 characters")
 68	}
 69
 70	return nil
 71}
 72
 73// isValidEmail checks email format (private)
 74func isValidEmail(email string) bool {
 75	emailRegex := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
 76	matched, _ := regexp.MatchString(emailRegex, email)
 77	return matched
 78}
 79
 80// hashPassword simulates password hashing (private)
 81func hashPassword(password string) string {
 82	return "hashed_" + password
 83}
 84
 85// generateID simulates ID generation (private)
 86func generateID() int {
 87	return 12345
 88}
 89
 90func main() {
 91	// Create user using exported constructor
 92	newUser, err := NewUser("alice@example.com", "Alice Smith", "secret123")
 93	if err != nil {
 94		fmt.Printf("Error creating user: %v\n", err)
 95		return
 96	}
 97
 98	fmt.Printf("Created user: Email=%s, Name=%s\n", newUser.Email, newUser.Name)
 99
100	// Use exported methods
101	if err := newUser.UpdateEmail("alice.new@example.com"); err != nil {
102		fmt.Printf("Error updating email: %v\n", err)
103	} else {
104		fmt.Printf("Updated email: %s\n", newUser.GetEmail())
105	}
106
107	// Verify password
108	if newUser.CheckPassword("secret123") {
109		fmt.Println("Password verification: SUCCESS")
110	} else {
111		fmt.Println("Password verification: FAILED")
112	}
113}

Key Concepts Demonstrated:

  • Encapsulation: Private fields hidden from external code
  • Controlled access: Methods manage field access
  • Input validation: Private helpers ensure data integrity
  • Clean constructor: NewUser() creates properly initialized instances

Example 3: Multi-Package Project Structure

Let's build a realistic project structure showing how packages work together:

  1// run
  2package main
  3
  4import (
  5	"fmt"
  6	"time"
  7)
  8
  9// Config represents application configuration
 10type Config struct {
 11	Port        string
 12	DatabaseURL string
 13	JWTSecret   string
 14	Debug       bool
 15}
 16
 17// LoadConfig simulates loading configuration
 18func LoadConfig() (*Config, error) {
 19	return &Config{
 20		Port:        "8080",
 21		DatabaseURL: "postgres://localhost:5432/mydb",
 22		JWTSecret:   "secret-key-123",
 23		Debug:       true,
 24	}, nil
 25}
 26
 27// Database connection simulation
 28type Database struct {
 29	url string
 30}
 31
 32func NewDatabase(url string) (*Database, error) {
 33	return &Database{url: url}, nil
 34}
 35
 36func (db *Database) Close() error {
 37	fmt.Println("Database connection closed")
 38	return nil
 39}
 40
 41func (db *Database) Ping() error {
 42	fmt.Printf("Database ping successful: %s\n", db.url)
 43	return nil
 44}
 45
 46// UserRepository simulates user data access
 47type UserRepository struct {
 48	db *Database
 49}
 50
 51func NewUserRepository(db *Database) *UserRepository {
 52	return &UserRepository{db: db}
 53}
 54
 55// UserService simulates user business logic
 56type UserService struct {
 57	repo *UserRepository
 58}
 59
 60func NewUserService(repo *UserRepository) *UserService {
 61	return &UserService{repo: repo}
 62}
 63
 64// Router simulates HTTP routing
 65type Router struct {
 66	userService *UserService
 67	jwtSecret   string
 68}
 69
 70func NewRouter(userService *UserService, jwtSecret string) *Router {
 71	return &Router{
 72		userService: userService,
 73		jwtSecret:   jwtSecret,
 74	}
 75}
 76
 77func (r *Router) RouteCount() int {
 78	return 10 // Simulated route count
 79}
 80
 81func main() {
 82	// Load configuration
 83	cfg, err := LoadConfig()
 84	if err != nil {
 85		fmt.Printf("Failed to load config: %v\n", err)
 86		return
 87	}
 88
 89	// Initialize database
 90	db, err := NewDatabase(cfg.DatabaseURL)
 91	if err != nil {
 92		fmt.Printf("Failed to connect to database: %v\n", err)
 93		return
 94	}
 95	defer db.Close()
 96
 97	// Test database connection
 98	if err := db.Ping(); err != nil {
 99		fmt.Printf("Database ping failed: %v\n", err)
100		return
101	}
102
103	// Create repository and service layers
104	userRepo := NewUserRepository(db)
105	userService := NewUserService(userRepo)
106
107	// Create HTTP router
108	router := NewRouter(userService, cfg.JWTSecret)
109
110	fmt.Printf("Server starting on port %s\n", cfg.Port)
111	fmt.Printf("Database connected: %s\n", cfg.DatabaseURL)
112	fmt.Printf("Handlers configured for %d routes\n", router.RouteCount())
113	fmt.Printf("Debug mode: %t\n", cfg.Debug)
114}

What this demonstrates:

  • Separation of concerns: Each component has clear responsibility
  • Dependency injection: Components are composed together
  • Clean interfaces: Packages expose minimal, focused APIs
  • Testable design: Each layer can be tested independently

Common Patterns and Pitfalls

Pattern 1: Domain-Driven Package Organization

Organize packages by business domain rather than technical layers:

Good: Domain-driven organization

myproject/
├── user/           # All user-related functionality
│   ├── user.go      # User model and business logic
│   ├── repository.go # Data access for users
│   └── service.go  # User service layer
├── order/          # All order-related functionality
│   ├── order.go
│   ├── repository.go
│   └── service.go
├── payment/        # All payment-related functionality
│   ├── payment.go
│   ├── processor.go
│   └── gateway.go
└── shipping/       # All shipping-related functionality
    ├── shipping.go
    └── calculator.go

Avoid: Technical layer organization

myproject/
├── models/         # Data models
├── repositories/    # Data access
├── services/       # Business logic
└── controllers/    # HTTP handlers

Benefits of Domain-Driven:

  • Clear ownership: Each team owns specific domain packages
  • Low coupling: Domains interact through well-defined interfaces
  • Easy testing: Each domain can be tested in isolation
  • Scalable teams: Multiple teams can work on different domains

Pattern 2: The internal Directory for Private Code

Use Go's special internal/ directory for implementation details:

myproject/
├── cmd/                    # Application entry points
│   ├── server/main.go
│   └── worker/main.go
├── internal/               # Private code
│   ├── auth/              # Authentication logic
│   │   ├── auth.go
│   │   └── middleware.go
│   ├── database/          # Database operations
│   │   ├── connection.go
│   │   └── migrations.go
│   └── business/          # Business logic
│       ├── users.go
│       └── orders.go
├── pkg/                    # Public reusable code
│   ├── logger/            # Logging utilities
│   │   └── logger.go
│   └── validator/         # Validation utilities
│       └── validator.go
├── api/                    # API specifications
│   └── openapi.yaml
└── go.mod

Why this works:

  • internal/ packages can only be imported by code in parent directories
  • External projects cannot import myproject/internal/auth
  • Prevents exposing implementation details
  • Forces clean API boundaries

Pattern 3: Package Naming Conventions

Follow Go's established naming conventions:

 1// Good: Short, descriptive, lowercase names
 2package http          // Standard library
 3package json          // Standard library
 4package strings       // Standard library
 5package validator     // Third-party package
 6package logger        // Your package
 7
 8// Avoid: Poor naming practices
 9// package HTTP          // Not lowercase
10// package my_package    // Uses underscores
11// package utility       // Too generic
12// package data          // Too vague
13// package helpers       // Not descriptive

Naming Guidelines:

  1. Lowercase single words when possible
  2. Descriptive but concise - validator not validations
  3. Avoid abbreviations - httpclient not httpcli
  4. Use domain names - auth, user, payment
  5. Follow Go standard library style

Pattern 4: Import Organization

Organize imports clearly and consistently:

 1// run
 2package main
 3
 4import (
 5	// Standard library imports
 6	"context"
 7	"fmt"
 8	"net/http"
 9	"time"
10)
11
12func main() {
13	fmt.Println("=== Import Organization Example ===")
14
15	// Create a context with timeout
16	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
17	defer cancel()
18
19	// Simulate HTTP request
20	req, err := http.NewRequestWithContext(ctx, "GET", "https://example.com", nil)
21	if err != nil {
22		fmt.Printf("Error creating request: %v\n", err)
23		return
24	}
25
26	fmt.Printf("Request created: %s %s\n", req.Method, req.URL)
27	fmt.Println("Imports are organized: stdlib first, then third-party, then local")
28}

Import Organization Best Practices:

  1. Standard library imports first
  2. Third-party imports second
  3. Local imports last
  4. Blank line between groups
  5. Alphabetically sorted within each group

Pitfall 1: Circular Imports

Go prevents circular imports at compile time. Here's how to avoid them:

Problem: Circular dependency

1// Package auth imports user
2// package auth
3// import "myproject/user"
4
5// Package user imports auth
6// package user
7// import "myproject/auth"

Solution: Use shared interfaces or move common logic

 1// run
 2package main
 3
 4import "fmt"
 5
 6// Common package with shared interfaces
 7type UserValidator interface {
 8	ValidateUser(userID int) bool
 9	ValidateEmail(email string) bool
10}
11
12// Auth package would implement validation
13type AuthService struct{}
14
15func (a *AuthService) ValidateUser(userID int) bool {
16	return userID > 0
17}
18
19func (a *AuthService) ValidateEmail(email string) bool {
20	return len(email) > 0
21}
22
23// User service depends on interface, not concrete type
24type UserService struct {
25	validator UserValidator
26}
27
28func NewUserService(validator UserValidator) *UserService {
29	return &UserService{validator: validator}
30}
31
32func (u *UserService) CreateUser(email string) error {
33	if !u.validator.ValidateEmail(email) {
34		return fmt.Errorf("invalid email")
35	}
36	fmt.Printf("User created with email: %s\n", email)
37	return nil
38}
39
40func main() {
41	auth := &AuthService{}
42	userService := NewUserService(auth)
43
44	if err := userService.CreateUser("test@example.com"); err != nil {
45		fmt.Printf("Error: %v\n", err)
46	}
47}

Pitfall 2: Interface Pollution

Don't create interfaces until you actually need them:

 1// run
 2package main
 3
 4import "fmt"
 5
 6// Avoid: Unnecessary interface when only one implementation exists
 7// type UserRepository interface {
 8//     Save(user *User) error
 9//     Find(id int) (*User, error)
10// }
11
12// Better: Use concrete types directly
13type User struct {
14	ID   int
15	Name string
16}
17
18type UserRepository struct {
19	users map[int]*User
20}
21
22func NewUserRepository() *UserRepository {
23	return &UserRepository{
24		users: make(map[int]*User),
25	}
26}
27
28func (r *UserRepository) Save(user *User) error {
29	r.users[user.ID] = user
30	return nil
31}
32
33func (r *UserRepository) Find(id int) (*User, error) {
34	user, exists := r.users[id]
35	if !exists {
36		return nil, fmt.Errorf("user not found")
37	}
38	return user, nil
39}
40
41func main() {
42	repo := NewUserRepository()
43
44	user := &User{ID: 1, Name: "Alice"}
45	repo.Save(user)
46
47	found, err := repo.Find(1)
48	if err != nil {
49		fmt.Printf("Error: %v\n", err)
50		return
51	}
52
53	fmt.Printf("Found user: %+v\n", found)
54}

Rule of thumb: Wait until you have 2+ implementations before creating an interface.

Pitfall 3: God Packages

Avoid packages that do too much:

 1// run
 2package main
 3
 4import "fmt"
 5
 6// Avoid: God package with too many responsibilities
 7// package utils would contain:
 8// - User management
 9// - Payment processing
10// - Email sending
11// - Reporting
12// - Validation
13
14// Better: Focused packages
15type UserManager struct{}
16
17func (u *UserManager) CreateUser(name string) {
18	fmt.Printf("User created: %s\n", name)
19}
20
21type PaymentProcessor struct{}
22
23func (p *PaymentProcessor) ProcessPayment(amount float64) {
24	fmt.Printf("Payment processed: $%.2f\n", amount)
25}
26
27type EmailSender struct{}
28
29func (e *EmailSender) SendEmail(to, subject string) {
30	fmt.Printf("Email sent to %s: %s\n", to, subject)
31}
32
33func main() {
34	userMgr := &UserManager{}
35	userMgr.CreateUser("Alice")
36
37	payment := &PaymentProcessor{}
38	payment.ProcessPayment(99.99)
39
40	email := &EmailSender{}
41	email.SendEmail("alice@example.com", "Welcome!")
42}

Integration and Mastery - Building Real Applications

Master Example: Complete Web Application Architecture

Let's build a complete mini e-commerce application showing advanced package organization:

  1// run
  2package main
  3
  4import (
  5	"context"
  6	"fmt"
  7	"os"
  8	"os/signal"
  9	"sync"
 10	"syscall"
 11	"time"
 12)
 13
 14// Config represents application configuration
 15type Config struct {
 16	Port              string
 17	DatabaseURL       string
 18	JWTSecret         string
 19	JWTExpiration     time.Duration
 20	SMTPServer        string
 21	Environment       string
 22	ServiceName       string
 23	ServiceVersion    string
 24	LogLevel          string
 25	LogFormat         string
 26	HealthPort        string
 27	MaxDBConnections  int
 28	RecommendEngine   string
 29}
 30
 31// LoadFromEnv loads configuration from environment
 32func LoadFromEnv() (*Config, error) {
 33	return &Config{
 34		Port:              "8080",
 35		DatabaseURL:       "postgres://localhost:5432/ecommerce",
 36		JWTSecret:         "secret-jwt-key",
 37		JWTExpiration:     24 * time.Hour,
 38		SMTPServer:        "smtp.example.com",
 39		Environment:       "development",
 40		ServiceName:       "ecommerce-api",
 41		ServiceVersion:    "1.0.0",
 42		LogLevel:          "info",
 43		LogFormat:         "json",
 44		HealthPort:        "8081",
 45		MaxDBConnections:  10,
 46		RecommendEngine:   "collaborative",
 47	}, nil
 48}
 49
 50// Logger simulates structured logging
 51type Logger struct {
 52	level  string
 53	format string
 54}
 55
 56func NewLogger(level, format string) (*Logger, error) {
 57	return &Logger{level: level, format: format}, nil
 58}
 59
 60func (l *Logger) Info(msg string, args ...interface{}) {
 61	fmt.Printf("[INFO] %s\n", fmt.Sprintf(msg, args...))
 62}
 63
 64func (l *Logger) Error(msg string, args ...interface{}) {
 65	fmt.Printf("[ERROR] %s\n", fmt.Sprintf(msg, args...))
 66}
 67
 68// Database simulates database connection
 69type Database struct {
 70	url string
 71}
 72
 73func NewDatabase(url string, maxConns int) (*Database, error) {
 74	return &Database{url: url}, nil
 75}
 76
 77func (db *Database) Migrate(ctx context.Context) error {
 78	fmt.Println("Running database migrations...")
 79	time.Sleep(100 * time.Millisecond)
 80	return nil
 81}
 82
 83func (db *Database) Close() error {
 84	fmt.Println("Database connection closed")
 85	return nil
 86}
 87
 88// AuthService simulates authentication
 89type AuthService struct {
 90	db         *Database
 91	jwtSecret  string
 92	expiration time.Duration
 93}
 94
 95func NewAuthService(db *Database, secret string, exp time.Duration) *AuthService {
 96	return &AuthService{
 97		db:         db,
 98		jwtSecret:  secret,
 99		expiration: exp,
100	}
101}
102
103// EmailService simulates email sending
104type EmailService struct {
105	smtpServer string
106}
107
108func NewEmailService(smtp string) *EmailService {
109	return &EmailService{smtpServer: smtp}
110}
111
112// RecommendationService simulates product recommendations
113type RecommendationService struct {
114	db     *Database
115	engine string
116}
117
118func NewRecommendationService(db *Database, engine string) *RecommendationService {
119	return &RecommendationService{db: db, engine: engine}
120}
121
122func (r *RecommendationService) Start(ctx context.Context) error {
123	fmt.Printf("Starting recommendation engine: %s\n", r.engine)
124	return nil
125}
126
127func (r *RecommendationService) Shutdown(ctx context.Context) error {
128	fmt.Println("Shutting down recommendation engine")
129	return nil
130}
131
132// APIServer simulates HTTP server
133type APIServer struct {
134	config *Config
135	logger *Logger
136	auth   *AuthService
137	email  *EmailService
138	recomm *RecommendationService
139}
140
141func NewAPIServer(cfg *Config, logger *Logger, auth *AuthService, email *EmailService, recomm *RecommendationService) *APIServer {
142	return &APIServer{
143		config: cfg,
144		logger: logger,
145		auth:   auth,
146		email:  email,
147		recomm: recomm,
148	}
149}
150
151func (s *APIServer) Start(ctx context.Context) error {
152	s.logger.Info("API server started on port %s", s.config.Port)
153	<-ctx.Done()
154	return nil
155}
156
157func (s *APIServer) Shutdown(ctx context.Context) error {
158	s.logger.Info("Shutting down API server")
159	return nil
160}
161
162// HealthChecker simulates health check endpoint
163type HealthChecker struct {
164	db          *Database
165	environment string
166	serviceName string
167	version     string
168}
169
170func NewHealthChecker(db *Database, env, name, version string) *HealthChecker {
171	return &HealthChecker{
172		db:          db,
173		environment: env,
174		serviceName: name,
175		version:     version,
176	}
177}
178
179func (h *HealthChecker) Start(ctx context.Context, port string) error {
180	fmt.Printf("Health check server started on port %s\n", port)
181	<-ctx.Done()
182	return nil
183}
184
185// Application orchestrates all components
186type Application struct {
187	config    *Config
188	server    *APIServer
189	database  *Database
190	auth      *AuthService
191	email     *EmailService
192	recomm    *RecommendationService
193	health    *HealthChecker
194	logger    *Logger
195}
196
197func NewApplication() (*Application, error) {
198	// Load configuration
199	cfg, err := LoadFromEnv()
200	if err != nil {
201		return nil, fmt.Errorf("failed to load config: %w", err)
202	}
203
204	// Initialize logger
205	logger, err := NewLogger(cfg.LogLevel, cfg.LogFormat)
206	if err != nil {
207		return nil, fmt.Errorf("failed to create logger: %w", err)
208	}
209
210	// Connect to database
211	db, err := NewDatabase(cfg.DatabaseURL, cfg.MaxDBConnections)
212	if err != nil {
213		return nil, fmt.Errorf("failed to connect to database: %w", err)
214	}
215
216	// Initialize services
217	auth := NewAuthService(db, cfg.JWTSecret, cfg.JWTExpiration)
218	email := NewEmailService(cfg.SMTPServer)
219	recomm := NewRecommendationService(db, cfg.RecommendEngine)
220
221	// Create API server
222	server := NewAPIServer(cfg, logger, auth, email, recomm)
223
224	// Create health checker
225	health := NewHealthChecker(db, cfg.Environment, cfg.ServiceName, cfg.ServiceVersion)
226
227	return &Application{
228		config:    cfg,
229		server:    server,
230		database:  db,
231		auth:      auth,
232		email:     email,
233		recomm:    recomm,
234		health:    health,
235		logger:    logger,
236	}, nil
237}
238
239func (app *Application) Start(ctx context.Context) error {
240	app.logger.Info("Starting application: %s v%s", app.config.ServiceName, app.config.ServiceVersion)
241	app.logger.Info("Environment: %s", app.config.Environment)
242	app.logger.Info("Port: %s", app.config.Port)
243
244	// Run database migration
245	go func() {
246		if err := app.database.Migrate(ctx); err != nil {
247			app.logger.Error("Database migration failed: %v", err)
248		}
249	}()
250
251	// Start recommendation engine
252	if err := app.recomm.Start(ctx); err != nil {
253		return fmt.Errorf("failed to start recommendation engine: %w", err)
254	}
255
256	// Start health check server
257	go func() {
258		app.logger.Info("Starting health check server on port %s", app.config.HealthPort)
259		if err := app.health.Start(ctx, app.config.HealthPort); err != nil {
260			app.logger.Error("Health check server failed: %v", err)
261		}
262	}()
263
264	// Start main API server
265	app.logger.Info("Starting API server on port %s", app.config.Port)
266	return app.server.Start(ctx)
267}
268
269func (app *Application) Shutdown(ctx context.Context) error {
270	app.logger.Info("Shutting down application")
271
272	shutdownCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
273	defer cancel()
274
275	var wg sync.WaitGroup
276
277	// Shutdown server
278	wg.Add(1)
279	go func() {
280		defer wg.Done()
281		if err := app.server.Shutdown(shutdownCtx); err != nil {
282			app.logger.Error("Server shutdown failed: %v", err)
283		}
284	}()
285
286	// Shutdown recommendation engine
287	wg.Add(1)
288	go func() {
289		defer wg.Done()
290		if err := app.recomm.Shutdown(shutdownCtx); err != nil {
291			app.logger.Error("Recommendation engine shutdown failed: %v", err)
292		}
293	}()
294
295	// Close database
296	wg.Add(1)
297	go func() {
298		defer wg.Done()
299		if err := app.database.Close(); err != nil {
300			app.logger.Error("Database close failed: %v", err)
301		}
302	}()
303
304	wg.Wait()
305
306	app.logger.Info("Application shutdown complete")
307	return nil
308}
309
310func main() {
311	// Create application
312	app, err := NewApplication()
313	if err != nil {
314		fmt.Printf("Failed to create application: %v\n", err)
315		os.Exit(1)
316	}
317
318	// Setup graceful shutdown
319	ctx, cancel := context.WithCancel(context.Background())
320	defer cancel()
321
322	// Handle signals
323	sigChan := make(chan os.Signal, 1)
324	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
325
326	// Start application
327	errChan := make(chan error, 1)
328	go func() {
329		if err := app.Start(ctx); err != nil {
330			errChan <- fmt.Errorf("application failed: %w", err)
331		}
332	}()
333
334	// Wait for signal or error
335	select {
336	case sig := <-sigChan:
337		app.logger.Info("Received shutdown signal: %s", sig.String())
338		cancel()
339	case err := <-errChan:
340		app.logger.Error("Application error: %v", err)
341		cancel()
342	case <-time.After(2 * time.Second):
343		// Simulate running for 2 seconds then shutdown
344		fmt.Println("\n=== Simulating graceful shutdown ===")
345		cancel()
346	}
347
348	// Graceful shutdown
349	if err := app.Shutdown(ctx); err != nil {
350		fmt.Printf("Failed to shutdown application: %v\n", err)
351		os.Exit(1)
352	}
353}

Key Concepts Demonstrated:

  • Clean architecture: Clear separation between layers
  • Dependency injection: Services composed through constructors
  • Graceful shutdown: Proper cleanup of all resources
  • Error handling: Structured error propagation
  • Configuration management: Environment-based configuration
  • Observability: Structured logging and health checks
  • Scalability: Organized for team development

Practice Exercises

Exercise 1: Basic Package Creation and Usage

Learning Objectives: Create a simple utility package with proper visibility control and basic functionality

Difficulty: Beginner

Real-World Context: Building reusable string manipulation utilities that demonstrate package organization and export control

Task: Create a string utilities package with functions for capitalizing, reversing, counting words, and validating emails. Implement both public and private functions to demonstrate visibility control.

Show Solution
 1// run
 2package main
 3
 4import (
 5	"fmt"
 6	"regexp"
 7	"strings"
 8	"unicode"
 9)
10
11// CapitalizeFirst capitalizes the first character
12func CapitalizeFirst(s string) string {
13	if s == "" {
14		return s
15	}
16	runes := []rune(s)
17	runes[0] = unicode.ToUpper(runes[0])
18	return string(runes)
19}
20
21// Reverse reverses a string
22func Reverse(s string) string {
23	runes := []rune(s)
24	for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
25		runes[i], runes[j] = runes[j], runes[i]
26	}
27	return string(runes)
28}
29
30// WordCount counts words in a string
31func WordCount(s string) int {
32	if s == "" {
33		return 0
34	}
35	return len(strings.Fields(s))
36}
37
38// IsValidEmail validates email format
39func IsValidEmail(email string) bool {
40	if email == "" {
41		return false
42	}
43	emailRegex := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
44	matched, _ := regexp.MatchString(emailRegex, email)
45	return matched
46}
47
48func main() {
49	text := "hello world"
50
51	fmt.Printf("Original: %s\n", text)
52	fmt.Printf("Capitalized: %s\n", CapitalizeFirst(text))
53	fmt.Printf("Reversed: %s\n", Reverse(text))
54	fmt.Printf("Word count: %d\n", WordCount(text))
55
56	fmt.Println("\n=== Email Validation ===")
57	emails := []string{
58		"test@example.com",
59		"invalid-email",
60		"user@domain.co.uk",
61		"@nodomain.com",
62	}
63
64	for _, email := range emails {
65		fmt.Printf("%s: %t\n", email, IsValidEmail(email))
66	}
67}

Exercise 2: Package with Structs and Method Receivers

Learning Objectives: Build a configuration package demonstrating proper struct design, method receivers, and validation

Difficulty: Intermediate

Real-World Context: Creating a type-safe configuration system that loads settings from environment with validation

Task: Create a Config struct with private fields and public methods. Implement LoadFromEnv() that validates all settings. Add methods for accessing and modifying configuration safely.

Show Solution
  1// run
  2package main
  3
  4import (
  5	"fmt"
  6	"regexp"
  7	"strconv"
  8	"time"
  9)
 10
 11// Config holds application configuration
 12type Config struct {
 13	databaseURL string
 14	serverPort  string
 15	jwtSecret   string
 16	debug       bool
 17	logLevel    string
 18	timeout     int
 19}
 20
 21// LoadFromEnv simulates loading from environment
 22func LoadFromEnv() (*Config, error) {
 23	cfg := &Config{
 24		databaseURL: "postgres://localhost:5432/mydb",
 25		serverPort:  "8080",
 26		jwtSecret:   "my-secret-jwt-key-123",
 27		debug:       true,
 28		logLevel:    "info",
 29		timeout:     30,
 30	}
 31
 32	if err := cfg.Validate(); err != nil {
 33		return nil, fmt.Errorf("invalid configuration: %w", err)
 34	}
 35
 36	return cfg, nil
 37}
 38
 39// DatabaseURL returns the database URL
 40func (c *Config) DatabaseURL() string {
 41	return c.databaseURL
 42}
 43
 44// ServerPort returns the server port
 45func (c *Config) ServerPort() string {
 46	return c.serverPort
 47}
 48
 49// JWTSecret returns the JWT secret
 50func (c *Config) JWTSecret() string {
 51	return c.jwtSecret
 52}
 53
 54// IsDebug returns debug mode status
 55func (c *Config) IsDebug() bool {
 56	return c.debug
 57}
 58
 59// LogLevel returns log level
 60func (c *Config) LogLevel() string {
 61	return c.logLevel
 62}
 63
 64// Timeout returns timeout duration
 65func (c *Config) Timeout() time.Duration {
 66	return time.Duration(c.timeout) * time.Second
 67}
 68
 69// SetDatabaseURL updates database URL with validation
 70func (c *Config) SetDatabaseURL(url string) error {
 71	if !isValidDatabaseURL(url) {
 72		return fmt.Errorf("invalid database URL format")
 73	}
 74	c.databaseURL = url
 75	return nil
 76}
 77
 78// Validate validates the configuration
 79func (c *Config) Validate() error {
 80	if c.databaseURL == "" {
 81		return fmt.Errorf("database URL is required")
 82	}
 83
 84	if !isValidDatabaseURL(c.databaseURL) {
 85		return fmt.Errorf("invalid database URL format")
 86	}
 87
 88	if c.serverPort == "" {
 89		return fmt.Errorf("server port is required")
 90	}
 91
 92	if !isValidPort(c.serverPort) {
 93		return fmt.Errorf("invalid server port: %s", c.serverPort)
 94	}
 95
 96	if c.jwtSecret == "" {
 97		return fmt.Errorf("JWT secret is required")
 98	}
 99
100	if len(c.jwtSecret) < 16 {
101		return fmt.Errorf("JWT secret must be at least 16 characters")
102	}
103
104	return nil
105}
106
107// isValidDatabaseURL validates database URL (private)
108func isValidDatabaseURL(url string) bool {
109	postgresPattern := `^postgres://.*`
110	mysqlPattern := `^mysql://.*`
111
112	postgresMatch, _ := regexp.MatchString(postgresPattern, url)
113	mysqlMatch, _ := regexp.MatchString(mysqlPattern, url)
114
115	return postgresMatch || mysqlMatch
116}
117
118// isValidPort validates port number (private)
119func isValidPort(port string) bool {
120	portNum, err := strconv.Atoi(port)
121	if err != nil {
122		return false
123	}
124	return portNum >= 1 && portNum <= 65535
125}
126
127func main() {
128	// Load configuration
129	cfg, err := LoadFromEnv()
130	if err != nil {
131		fmt.Printf("Error loading config: %v\n", err)
132		return
133	}
134
135	fmt.Println("=== Configuration Loaded ===")
136	fmt.Printf("Database URL: %s\n", cfg.DatabaseURL())
137	fmt.Printf("Server Port: %s\n", cfg.ServerPort())
138	fmt.Printf("Debug Mode: %t\n", cfg.IsDebug())
139	fmt.Printf("Log Level: %s\n", cfg.LogLevel())
140	fmt.Printf("Timeout: %v\n", cfg.Timeout())
141	fmt.Printf("JWT Secret: [REDACTED]\n")
142
143	// Test validation
144	fmt.Println("\n=== Testing Validation ===")
145	testCfg := &Config{
146		databaseURL: "invalid-url",
147		serverPort:  "8080",
148		jwtSecret:   "short",
149	}
150
151	if err := testCfg.Validate(); err != nil {
152		fmt.Printf("Validation error (expected): %v\n", err)
153	}
154
155	// Test update method
156	fmt.Println("\n=== Testing Update Method ===")
157	if err := cfg.SetDatabaseURL("postgres://newhost:5432/newdb"); err != nil {
158		fmt.Printf("Error: %v\n", err)
159	} else {
160		fmt.Printf("Updated database URL: %s\n", cfg.DatabaseURL())
161	}
162}

Exercise 3: Multi-Package Application with Interfaces

Learning Objectives: Create a complete mini-application with multiple packages using interfaces for clean separation of concerns

Difficulty: Advanced

Real-World Context: Building a layered application architecture with repositories, services, and HTTP handlers

Task: Create models, database repository, service layer, and API handlers. Use interfaces to enable dependency injection and testability.

Show Solution
  1// run
  2package main
  3
  4import (
  5	"fmt"
  6	"time"
  7)
  8
  9// User model
 10type User struct {
 11	ID        int
 12	Email     string
 13	Name      string
 14	CreatedAt time.Time
 15}
 16
 17// Product model
 18type Product struct {
 19	ID    int
 20	Name  string
 21	Price float64
 22}
 23
 24// UserRepository interface
 25type UserRepository interface {
 26	Create(user *User) error
 27	FindByID(id int) (*User, error)
 28	FindByEmail(email string) (*User, error)
 29	Update(user *User) error
 30	Delete(id int) error
 31}
 32
 33// ProductRepository interface
 34type ProductRepository interface {
 35	Create(product *Product) error
 36	FindByID(id int) (*Product, error)
 37	List() ([]*Product, error)
 38}
 39
 40// SQLUserRepository implements UserRepository
 41type SQLUserRepository struct {
 42	users  map[int]*User
 43	nextID int
 44}
 45
 46func NewUserRepository() UserRepository {
 47	return &SQLUserRepository{
 48		users:  make(map[int]*User),
 49		nextID: 1,
 50	}
 51}
 52
 53func (r *SQLUserRepository) Create(user *User) error {
 54	user.ID = r.nextID
 55	user.CreatedAt = time.Now()
 56	r.users[user.ID] = user
 57	r.nextID++
 58	return nil
 59}
 60
 61func (r *SQLUserRepository) FindByID(id int) (*User, error) {
 62	user, exists := r.users[id]
 63	if !exists {
 64		return nil, fmt.Errorf("user not found")
 65	}
 66	return user, nil
 67}
 68
 69func (r *SQLUserRepository) FindByEmail(email string) (*User, error) {
 70	for _, user := range r.users {
 71		if user.Email == email {
 72			return user, nil
 73		}
 74	}
 75	return nil, fmt.Errorf("user not found")
 76}
 77
 78func (r *SQLUserRepository) Update(user *User) error {
 79	if _, exists := r.users[user.ID]; !exists {
 80		return fmt.Errorf("user not found")
 81	}
 82	r.users[user.ID] = user
 83	return nil
 84}
 85
 86func (r *SQLUserRepository) Delete(id int) error {
 87	delete(r.users, id)
 88	return nil
 89}
 90
 91// SQLProductRepository implements ProductRepository
 92type SQLProductRepository struct {
 93	products map[int]*Product
 94	nextID   int
 95}
 96
 97func NewProductRepository() ProductRepository {
 98	return &SQLProductRepository{
 99		products: make(map[int]*Product),
100		nextID:   1,
101	}
102}
103
104func (r *SQLProductRepository) Create(product *Product) error {
105	product.ID = r.nextID
106	r.products[product.ID] = product
107	r.nextID++
108	return nil
109}
110
111func (r *SQLProductRepository) FindByID(id int) (*Product, error) {
112	product, exists := r.products[id]
113	if !exists {
114		return nil, fmt.Errorf("product not found")
115	}
116	return product, nil
117}
118
119func (r *SQLProductRepository) List() ([]*Product, error) {
120	products := make([]*Product, 0, len(r.products))
121	for _, p := range r.products {
122		products = append(products, p)
123	}
124	return products, nil
125}
126
127// UserService provides business logic
128type UserService struct {
129	userRepo UserRepository
130}
131
132func NewUserService(repo UserRepository) *UserService {
133	return &UserService{userRepo: repo}
134}
135
136func (s *UserService) CreateUser(name, email string) (*User, error) {
137	if name == "" || email == "" {
138		return nil, fmt.Errorf("name and email are required")
139	}
140
141	// Check if user exists
142	if existing, _ := s.userRepo.FindByEmail(email); existing != nil {
143		return nil, fmt.Errorf("user with email %s already exists", email)
144	}
145
146	user := &User{
147		Name:  name,
148		Email: email,
149	}
150
151	if err := s.userRepo.Create(user); err != nil {
152		return nil, fmt.Errorf("failed to create user: %w", err)
153	}
154
155	return user, nil
156}
157
158func (s *UserService) GetUser(id int) (*User, error) {
159	return s.userRepo.FindByID(id)
160}
161
162// ProductService provides business logic
163type ProductService struct {
164	productRepo ProductRepository
165}
166
167func NewProductService(repo ProductRepository) *ProductService {
168	return &ProductService{productRepo: repo}
169}
170
171func (s *ProductService) CreateProduct(name string, price float64) (*Product, error) {
172	if name == "" {
173		return nil, fmt.Errorf("name is required")
174	}
175	if price <= 0 {
176		return nil, fmt.Errorf("price must be positive")
177	}
178
179	product := &Product{
180		Name:  name,
181		Price: price,
182	}
183
184	if err := s.productRepo.Create(product); err != nil {
185		return nil, fmt.Errorf("failed to create product: %w", err)
186	}
187
188	return product, nil
189}
190
191func (s *ProductService) ListProducts() ([]*Product, error) {
192	return s.productRepo.List()
193}
194
195// APIServer simulates HTTP server
196type APIServer struct {
197	port           string
198	userService    *UserService
199	productService *ProductService
200}
201
202func NewAPIServer(port string, userSvc *UserService, productSvc *ProductService) *APIServer {
203	return &APIServer{
204		port:           port,
205		userService:    userSvc,
206		productService: productSvc,
207	}
208}
209
210func (s *APIServer) PrintRoutes() {
211	routes := []string{
212		"POST /api/users - Create user",
213		"GET /api/users/:id - Get user",
214		"POST /api/products - Create product",
215		"GET /api/products - List products",
216	}
217	fmt.Println("Available endpoints:")
218	for _, route := range routes {
219		fmt.Printf("  %s\n", route)
220	}
221}
222
223func (s *APIServer) Start() error {
224	fmt.Printf("Server would start on port %s\n", s.port)
225	return nil
226}
227
228func main() {
229	fmt.Println("=== Initializing Application ===")
230
231	// Create repositories
232	userRepo := NewUserRepository()
233	productRepo := NewProductRepository()
234
235	// Create services
236	userService := NewUserService(userRepo)
237	productService := NewProductService(productRepo)
238
239	// Create API server
240	server := NewAPIServer("8080", userService, productService)
241
242	fmt.Println("\n=== Creating Test Data ===")
243
244	// Create users
245	user1, _ := userService.CreateUser("Alice", "alice@example.com")
246	user2, _ := userService.CreateUser("Bob", "bob@example.com")
247	fmt.Printf("Created users: %+v, %+v\n", user1, user2)
248
249	// Create products
250	product1, _ := productService.CreateProduct("Laptop", 999.99)
251	product2, _ := productService.CreateProduct("Mouse", 29.99)
252	fmt.Printf("Created products: %+v, %+v\n", product1, product2)
253
254	// List all products
255	fmt.Println("\n=== Listing All Products ===")
256	products, _ := productService.ListProducts()
257	for _, p := range products {
258		fmt.Printf("Product: %s - $%.2f\n", p.Name, p.Price)
259	}
260
261	fmt.Println("\n=== Server Configuration ===")
262	server.PrintRoutes()
263	server.Start()
264}

Exercise 4: Package Organization Patterns

Learning Objectives: Apply domain-driven design principles to organize a realistic application

Difficulty: Advanced

Real-World Context: Structuring a blogging platform with users, posts, comments, and authentication

Task: Design and implement a multi-domain application structure. Create separate packages for user management, post management, comment management, and authentication. Demonstrate clean boundaries and dependencies.

Show Solution
  1// run
  2package main
  3
  4import (
  5	"fmt"
  6	"time"
  7)
  8
  9// User domain
 10type User struct {
 11	ID        int
 12	Username  string
 13	Email     string
 14	CreatedAt time.Time
 15}
 16
 17type UserService struct {
 18	users  map[int]*User
 19	nextID int
 20}
 21
 22func NewUserService() *UserService {
 23	return &UserService{
 24		users:  make(map[int]*User),
 25		nextID: 1,
 26	}
 27}
 28
 29func (s *UserService) CreateUser(username, email string) (*User, error) {
 30	user := &User{
 31		ID:        s.nextID,
 32		Username:  username,
 33		Email:     email,
 34		CreatedAt: time.Now(),
 35	}
 36	s.users[user.ID] = user
 37	s.nextID++
 38	return user, nil
 39}
 40
 41func (s *UserService) GetUser(id int) (*User, error) {
 42	user, exists := s.users[id]
 43	if !exists {
 44		return nil, fmt.Errorf("user not found")
 45	}
 46	return user, nil
 47}
 48
 49// Post domain
 50type Post struct {
 51	ID        int
 52	UserID    int
 53	Title     string
 54	Content   string
 55	CreatedAt time.Time
 56}
 57
 58type PostService struct {
 59	posts      map[int]*Post
 60	nextID     int
 61	userService *UserService
 62}
 63
 64func NewPostService(userSvc *UserService) *PostService {
 65	return &PostService{
 66		posts:      make(map[int]*Post),
 67		nextID:     1,
 68		userService: userSvc,
 69	}
 70}
 71
 72func (s *PostService) CreatePost(userID int, title, content string) (*Post, error) {
 73	// Verify user exists
 74	if _, err := s.userService.GetUser(userID); err != nil {
 75		return nil, fmt.Errorf("invalid user: %w", err)
 76	}
 77
 78	post := &Post{
 79		ID:        s.nextID,
 80		UserID:    userID,
 81		Title:     title,
 82		Content:   content,
 83		CreatedAt: time.Now(),
 84	}
 85	s.posts[post.ID] = post
 86	s.nextID++
 87	return post, nil
 88}
 89
 90func (s *PostService) GetPost(id int) (*Post, error) {
 91	post, exists := s.posts[id]
 92	if !exists {
 93		return nil, fmt.Errorf("post not found")
 94	}
 95	return post, nil
 96}
 97
 98func (s *PostService) ListUserPosts(userID int) ([]*Post, error) {
 99	var posts []*Post
100	for _, post := range s.posts {
101		if post.UserID == userID {
102			posts = append(posts, post)
103		}
104	}
105	return posts, nil
106}
107
108// Comment domain
109type Comment struct {
110	ID        int
111	PostID    int
112	UserID    int
113	Content   string
114	CreatedAt time.Time
115}
116
117type CommentService struct {
118	comments    map[int]*Comment
119	nextID      int
120	postService *PostService
121	userService *UserService
122}
123
124func NewCommentService(postSvc *PostService, userSvc *UserService) *CommentService {
125	return &CommentService{
126		comments:    make(map[int]*Comment),
127		nextID:      1,
128		postService: postSvc,
129		userService: userSvc,
130	}
131}
132
133func (s *CommentService) CreateComment(postID, userID int, content string) (*Comment, error) {
134	// Verify post exists
135	if _, err := s.postService.GetPost(postID); err != nil {
136		return nil, fmt.Errorf("invalid post: %w", err)
137	}
138
139	// Verify user exists
140	if _, err := s.userService.GetUser(userID); err != nil {
141		return nil, fmt.Errorf("invalid user: %w", err)
142	}
143
144	comment := &Comment{
145		ID:        s.nextID,
146		PostID:    postID,
147		UserID:    userID,
148		Content:   content,
149		CreatedAt: time.Now(),
150	}
151	s.comments[comment.ID] = comment
152	s.nextID++
153	return comment, nil
154}
155
156func (s *CommentService) ListPostComments(postID int) ([]*Comment, error) {
157	var comments []*Comment
158	for _, comment := range s.comments {
159		if comment.PostID == postID {
160			comments = append(comments, comment)
161		}
162	}
163	return comments, nil
164}
165
166// Auth domain
167type AuthService struct {
168	userService *UserService
169	sessions    map[string]int // token -> userID
170}
171
172func NewAuthService(userSvc *UserService) *AuthService {
173	return &AuthService{
174		userService: userSvc,
175		sessions:    make(map[string]int),
176	}
177}
178
179func (s *AuthService) Login(email string) (string, error) {
180	// Simplified: find user by email
181	for _, user := range s.userService.users {
182		if user.Email == email {
183			token := fmt.Sprintf("token_%d_%d", user.ID, time.Now().Unix())
184			s.sessions[token] = user.ID
185			return token, nil
186		}
187	}
188	return "", fmt.Errorf("authentication failed")
189}
190
191func (s *AuthService) ValidateToken(token string) (int, error) {
192	userID, exists := s.sessions[token]
193	if !exists {
194		return 0, fmt.Errorf("invalid token")
195	}
196	return userID, nil
197}
198
199func main() {
200	fmt.Println("=== Blogging Platform Demo ===")
201
202	// Initialize services
203	userSvc := NewUserService()
204	authSvc := NewAuthService(userSvc)
205	postSvc := NewPostService(userSvc)
206	commentSvc := NewCommentService(postSvc, userSvc)
207
208	// Create users
209	fmt.Println("\n1. Creating Users")
210	alice, _ := userSvc.CreateUser("alice", "alice@example.com")
211	bob, _ := userSvc.CreateUser("bob", "bob@example.com")
212	fmt.Printf("Created users: %s, %s\n", alice.Username, bob.Username)
213
214	// Authenticate user
215	fmt.Println("\n2. User Authentication")
216	token, _ := authSvc.Login("alice@example.com")
217	fmt.Printf("Alice logged in with token: %s\n", token)
218
219	// Validate token
220	userID, _ := authSvc.ValidateToken(token)
221	fmt.Printf("Token validated for user ID: %d\n", userID)
222
223	// Create posts
224	fmt.Println("\n3. Creating Posts")
225	post1, _ := postSvc.CreatePost(alice.ID, "My First Post", "Hello world!")
226	post2, _ := postSvc.CreatePost(alice.ID, "Go Programming", "Learning Go is fun!")
227	post3, _ := postSvc.CreatePost(bob.ID, "Bob's Post", "Nice platform!")
228	fmt.Printf("Created %d posts\n", 3)
229
230	// List user posts
231	fmt.Println("\n4. Listing Alice's Posts")
232	alicePosts, _ := postSvc.ListUserPosts(alice.ID)
233	for _, post := range alicePosts {
234		fmt.Printf("  - %s: %s\n", post.Title, post.Content)
235	}
236
237	// Create comments
238	fmt.Println("\n5. Creating Comments")
239	commentSvc.CreateComment(post1.ID, bob.ID, "Great first post!")
240	commentSvc.CreateComment(post1.ID, bob.ID, "Looking forward to more!")
241	commentSvc.CreateComment(post2.ID, alice.ID, "Thanks everyone!")
242
243	// List comments
244	fmt.Println("\n6. Listing Comments on First Post")
245	comments, _ := commentSvc.ListPostComments(post1.ID)
246	for _, comment := range comments {
247		user, _ := userSvc.GetUser(comment.UserID)
248		fmt.Printf("  - %s: %s\n", user.Username, comment.Content)
249	}
250
251	// Summary
252	fmt.Println("\n=== Summary ===")
253	fmt.Printf("Total Users: %d\n", len(userSvc.users))
254	fmt.Printf("Total Posts: %d\n", len(postSvc.posts))
255	fmt.Printf("Total Comments: %d\n", len(commentSvc.comments))
256	fmt.Printf("Active Sessions: %d\n", len(authSvc.sessions))
257}

Exercise 5: Advanced Package Design with Internal Directory

Learning Objectives: Master the use of internal packages for encapsulation and clean API boundaries

Difficulty: Advanced

Real-World Context: Building a library with public API and private implementation details

Task: Create a monitoring library with public API in root package and private implementation in internal/. Demonstrate how internal/ prevents external packages from importing implementation details.

Show Solution
  1// run
  2package main
  3
  4import (
  5	"fmt"
  6	"sync"
  7	"time"
  8)
  9
 10// Metric represents a monitoring metric
 11type Metric struct {
 12	Name      string
 13	Value     float64
 14	Timestamp time.Time
 15	Tags      map[string]string
 16}
 17
 18// MetricType represents different metric types
 19type MetricType int
 20
 21const (
 22	Counter MetricType = iota
 23	Gauge
 24	Histogram
 25)
 26
 27// Internal collector (would be in internal/ directory)
 28type metricCollector struct {
 29	mu      sync.RWMutex
 30	metrics map[string]*Metric
 31}
 32
 33func newMetricCollector() *metricCollector {
 34	return &metricCollector{
 35		metrics: make(map[string]*Metric),
 36	}
 37}
 38
 39func (c *metricCollector) record(metric *Metric) {
 40	c.mu.Lock()
 41	defer c.mu.Unlock()
 42	c.metrics[metric.Name] = metric
 43}
 44
 45func (c *metricCollector) get(name string) (*Metric, bool) {
 46	c.mu.RLock()
 47	defer c.mu.RUnlock()
 48	m, exists := c.metrics[name]
 49	return m, exists
 50}
 51
 52func (c *metricCollector) getAll() []*Metric {
 53	c.mu.RLock()
 54	defer c.mu.RUnlock()
 55
 56	metrics := make([]*Metric, 0, len(c.metrics))
 57	for _, m := range c.metrics {
 58		metrics = append(metrics, m)
 59	}
 60	return metrics
 61}
 62
 63// Internal aggregator (would be in internal/ directory)
 64type metricAggregator struct {
 65	collector *metricCollector
 66}
 67
 68func newMetricAggregator(collector *metricCollector) *metricAggregator {
 69	return &metricAggregator{collector: collector}
 70}
 71
 72func (a *metricAggregator) aggregate() map[string]float64 {
 73	metrics := a.collector.getAll()
 74	aggregated := make(map[string]float64)
 75
 76	for _, m := range metrics {
 77		aggregated[m.Name] += m.Value
 78	}
 79
 80	return aggregated
 81}
 82
 83// Monitor is the public API (would be in root package)
 84type Monitor struct {
 85	collector  *metricCollector
 86	aggregator *metricAggregator
 87	name       string
 88}
 89
 90// NewMonitor creates a new monitoring instance
 91func NewMonitor(name string) *Monitor {
 92	collector := newMetricCollector()
 93	aggregator := newMetricAggregator(collector)
 94
 95	return &Monitor{
 96		collector:  collector,
 97		aggregator: aggregator,
 98		name:       name,
 99	}
100}
101
102// RecordCounter records a counter metric
103func (m *Monitor) RecordCounter(name string, value float64, tags map[string]string) {
104	metric := &Metric{
105		Name:      name,
106		Value:     value,
107		Timestamp: time.Now(),
108		Tags:      tags,
109	}
110	m.collector.record(metric)
111}
112
113// RecordGauge records a gauge metric
114func (m *Monitor) RecordGauge(name string, value float64, tags map[string]string) {
115	metric := &Metric{
116		Name:      name,
117		Value:     value,
118		Timestamp: time.Now(),
119		Tags:      tags,
120	}
121	m.collector.record(metric)
122}
123
124// GetMetric retrieves a specific metric
125func (m *Monitor) GetMetric(name string) (*Metric, error) {
126	metric, exists := m.collector.get(name)
127	if !exists {
128		return nil, fmt.Errorf("metric %s not found", name)
129	}
130	return metric, nil
131}
132
133// GetAllMetrics retrieves all metrics
134func (m *Monitor) GetAllMetrics() []*Metric {
135	return m.collector.getAll()
136}
137
138// GetAggregatedMetrics returns aggregated metric values
139func (m *Monitor) GetAggregatedMetrics() map[string]float64 {
140	return m.aggregator.aggregate()
141}
142
143// ExportMetrics formats metrics for export
144func (m *Monitor) ExportMetrics() string {
145	metrics := m.collector.getAll()
146	result := fmt.Sprintf("=== Metrics Export: %s ===\n", m.name)
147
148	for _, metric := range metrics {
149		result += fmt.Sprintf("%s: %.2f (at %s)\n",
150			metric.Name,
151			metric.Value,
152			metric.Timestamp.Format("15:04:05"),
153		)
154
155		if len(metric.Tags) > 0 {
156			result += "  Tags: "
157			for k, v := range metric.Tags {
158				result += fmt.Sprintf("%s=%s ", k, v)
159			}
160			result += "\n"
161		}
162	}
163
164	return result
165}
166
167func main() {
168	fmt.Println("=== Monitoring Library Demo ===")
169
170	// Create monitor instance
171	monitor := NewMonitor("web-server")
172
173	// Record various metrics
174	fmt.Println("\n1. Recording Metrics")
175	monitor.RecordCounter("requests_total", 100, map[string]string{
176		"method": "GET",
177		"path":   "/api/users",
178	})
179
180	monitor.RecordCounter("requests_total", 50, map[string]string{
181		"method": "POST",
182		"path":   "/api/users",
183	})
184
185	monitor.RecordGauge("memory_usage_mb", 256.5, map[string]string{
186		"type": "heap",
187	})
188
189	monitor.RecordGauge("cpu_usage_percent", 45.2, map[string]string{
190		"core": "0",
191	})
192
193	monitor.RecordCounter("errors_total", 5, map[string]string{
194		"type": "validation",
195	})
196
197	// Retrieve specific metric
198	fmt.Println("\n2. Retrieving Specific Metric")
199	if metric, err := monitor.GetMetric("memory_usage_mb"); err == nil {
200		fmt.Printf("Memory Usage: %.2f MB\n", metric.Value)
201	}
202
203	// Get all metrics
204	fmt.Println("\n3. Listing All Metrics")
205	allMetrics := monitor.GetAllMetrics()
206	fmt.Printf("Total metrics recorded: %d\n", len(allMetrics))
207
208	// Get aggregated metrics
209	fmt.Println("\n4. Aggregated Metrics")
210	aggregated := monitor.GetAggregatedMetrics()
211	for name, value := range aggregated {
212		fmt.Printf("%s: %.2f\n", name, value)
213	}
214
215	// Export metrics
216	fmt.Println("\n5. Exporting Metrics")
217	exported := monitor.ExportMetrics()
218	fmt.Print(exported)
219
220	// Demonstrate that internal implementation is hidden
221	fmt.Println("\n=== Key Design Points ===")
222	fmt.Println("- Public API: Monitor struct with public methods")
223	fmt.Println("- Private implementation: metricCollector and metricAggregator")
224	fmt.Println("- Clean boundaries: Users cannot access internal details")
225	fmt.Println("- Thread-safe: Internal synchronization with mutexes")
226	fmt.Println("- Testable: Each component can be tested independently")
227}

Summary

Key Takeaways

Mastered Core Concepts:

  • Package Philosophy: Understand Go's simple yet powerful approach to code organization
  • Visibility Control: Master capitalization-based public/private access
  • Import Management: Organize dependencies efficiently and avoid circular imports
  • Package Design: Create focused, reusable, and maintainable packages
  • Project Structure: Apply proven patterns for large applications
  • Best Practices: Follow Go naming conventions and documentation standards

Real-World Benefits:

  • Team Collaboration: Different teams can work on different packages independently
  • Code Reusability: Share packages across projects and organizations
  • Maintainability: Find and fix issues quickly with well-organized code
  • Testing: Test individual packages in isolation with clear boundaries
  • Scalability: Organize codebases that grow gracefully with complexity

Critical Safety Rules:

  1. One package per directory - Go enforces this at compile time
  2. Use lowercase package names - Follow Go conventions
  3. Export intentionally - Only make public what's needed externally
  4. Avoid circular imports - They're caught at compile time but prevent good design
  5. Prefer domain-driven organization - Group by business capability, not technical layer
  6. Use internal/ for private code - Prevent external access to implementation details

Decision Matrix

Situation Package Structure Reason
Small utility functions Flat with utils/ package Simple, focused functionality
Web application Domain-driven with internal/ Clear boundaries, team collaboration
Library/framework Layered with clear APIs Reusable components
Microservice Domain-driven per service Independent deployment
Large enterprise Mixed: domains + shared pkg/ Balance reusability and encapsulation

Package Design Principles

  1. Single Responsibility: Each package has one clear purpose
  2. Clear Boundaries: Public API is minimal and well-defined
  3. Domain-Driven: Organize by business capability
  4. Dependency Direction: Dependencies point inward, not circular
  5. Testable Design: Each package can be tested independently
  6. Documentation: Public APIs are well-documented

Organization Patterns

Small Projects:

project/
├── main.go
├── utils/
└── config/

Medium Applications:

project/
├── cmd/
├── internal/
│   ├── handlers/
│   ├── models/
│   └── services/
└── pkg/
    ├── config/
    └── logger/

Large Systems:

project/
├── cmd/
├── internal/
│   ├── user/
│   ├── order/
│   ├── payment/
│   └── shipping/
├── pkg/
│   ├── database/
│   ├── cache/
│   └── messaging/
└── api/

Next Steps in Your Go Journey

Now that you've mastered packages, you're ready for:

  1. Go Modules: Learn dependency management and versioning with go.mod
  2. Testing: Master unit testing patterns for package-based code
  3. Build Systems: Understand Go's build tags and cross-compilation
  4. Architecture: Design larger systems using package principles
  5. Standard Library: Deep dive into Go's extensive standard library packages

Remember: Packages are your foundation for building maintainable Go applications. Well-designed packages make the difference between code that's a pleasure to work with and code that becomes a nightmare.

Master packages, and you'll build Go applications that scale naturally instead of collapsing under their own complexity.