Go Packages and Modules - Organizing and Sharing Code

Learning Objectives: By the end of this lesson, you will be able to:

  • Understand Go’s package system and how to organize code effectively
  • Create, import, and manage packages in Go applications
  • Master Go modules for dependency management and versioning
  • Design clean package APIs with proper encapsulation
  • Implement package-level initialization and configuration
  • Handle package dependencies and version conflicts
  • Publish and distribute Go packages to the community
  • Apply Go’s naming conventions and best practices for maintainable code

1. Welcome and Introduction (5 minutes)

Icebreaker Activity

Imagine building a house where every room contains everything you need - kitchen appliances in the bedroom, bathroom fixtures in the living room, and tools scattered everywhere. It would be chaotic and impossible to maintain! Go packages work like the rooms in a well-organized house - each package has a specific purpose, clear boundaries, and provides exactly what other parts of your application need.

Lesson Overview

  • Duration: 6-8 hours
  • Prerequisites: Go functions, structs, interfaces, and error handling
  • Materials Needed: Go development environment, understanding of code organization principles

2. Learning Objectives and Session Outline (5 minutes)

What We’ll Cover Today:

  1. Package Fundamentals - Understanding Go’s package system and organization
  2. Creating Packages - Building reusable, well-designed packages
  3. Import System - How Go resolves and loads packages
  4. Go Modules - Modern dependency management and versioning
  5. Package Design - API design, encapsulation, and best practices
  6. Initialization - Package-level setup and configuration
  7. Testing Packages - Unit testing and package validation
  8. Publishing - Sharing packages with the Go community

Success Criteria:

By the end of this session, you should be able to design well-structured Go applications using packages and modules, manage dependencies effectively, and create reusable packages for the community.

3. Foundation Knowledge (15-20 minutes)

Understanding Go’s Package Philosophy

Go’s package system is designed around simplicity, clarity, and dependency management. Every Go program is made up of packages, and the package is the unit of compilation and reuse.

Real-World Analogy: Think of Go packages like specialized departments in a company. Each department (package) has specific responsibilities, clear interfaces for communication, and can be managed independently while contributing to the overall organization.

Why This Matters: Well-organized packages provide:

  • Code Reusability: Write once, use everywhere
  • Maintainability: Clear boundaries make code easier to understand and modify
  • Testing: Packages can be tested in isolation
  • Collaboration: Teams can work on different packages independently
  • Distribution: Packages can be shared and versioned independently

Core Concepts

Package Basics

  • Package Declaration: Every Go file starts with a package declaration
  • Package Name: Should be concise, descriptive, and lowercase
  • Package Directory: Package name typically matches directory name
  • main Package: Special package for executable programs

Visibility Rules

  • Exported Names: Start with uppercase letter, visible outside package
  • Unexported Names: Start with lowercase letter, package-private
  • Package-level Scope: Variables, functions, types visible within package

Go Modules

  • Module: Collection of packages versioned together
  • go.mod File: Module definition with dependencies
  • Semantic Versioning: Major.minor.patch version scheme
  • Module Path: Unique identifier for modules (often repository URL)

Key Terminology

  • Package: Basic unit of code organization in Go
  • Module: Collection of packages with shared versioning
  • Import Path: String that identifies a package for import
  • Workspace: Directory containing Go source code
  • GOPATH: Legacy workspace directory (mostly replaced by modules)
  • go.mod: Module definition file
  • go.sum: Module checksum file for security
  • Vendor Directory: Local copy of dependencies

4. Detailed Explanation and Examples (25-30 minutes)

Understanding Package Organization in Go

Go’s package system encourages clean code organization by making dependencies explicit and enforcing encapsulation through visibility rules. This leads to more maintainable and testable code.

Package Design Principles

  • Single Responsibility: Each package should have one clear purpose
  • Minimal API Surface: Export only what’s necessary
  • Clear Naming: Package and exported names should be self-documenting
  • Dependency Direction: Higher-level packages depend on lower-level ones

Module System Benefits

  • Reproducible Builds: Exact dependency versions recorded
  • Security: Cryptographic checksums prevent tampering
  • Versioning: Semantic versioning enables safe updates
  • Decentralized: No central repository required

Practical Applications

Package organization is crucial for:

  • Microservices: Clear service boundaries and APIs
  • Libraries: Reusable components across projects
  • Large Applications: Managing complexity through modularity
  • Team Development: Parallel development on different packages
  • Open Source: Sharing code with the community

Common Misconceptions

  1. Misconception: Packages should be very small with single functions Reality: Packages should group related functionality logically

  2. Misconception: All packages need to be published separately Reality: Many packages are internal to applications and never published

  3. Misconception: Package names should include their purpose Reality: Package names should be concise; purpose is clear from context

5. Live Coding Demonstration (20-25 minutes)

Example 1: Creating and Using Basic Packages

// File: math/calculator.go
package math

import "errors"

// Calculator represents a basic calculator
type Calculator struct {
    precision int
}

// NewCalculator creates a new calculator with specified precision
func NewCalculator(precision int) *Calculator {
    if precision < 0 {
        precision = 2 // default precision
    }
    return &Calculator{precision: precision}
}

// Add performs addition
func (c *Calculator) Add(a, b float64) float64 {
    return c.round(a + b)
}

// Subtract performs subtraction
func (c *Calculator) Subtract(a, b float64) float64 {
    return c.round(a - b)
}

// Multiply performs multiplication
func (c *Calculator) Multiply(a, b float64) float64 {
    return c.round(a * b)
}

// Divide performs division
func (c *Calculator) Divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return c.round(a / b), nil
}

// round rounds to the calculator's precision (unexported helper)
func (c *Calculator) round(value float64) float64 {
    multiplier := 1.0
    for i := 0; i < c.precision; i++ {
        multiplier *= 10
    }
    return float64(int(value*multiplier+0.5)) / multiplier
}

// GetPrecision returns the current precision
func (c *Calculator) GetPrecision() int {
    return c.precision
}

// Package-level convenience functions
var defaultCalculator = NewCalculator(2)

// Add is a package-level convenience function
func Add(a, b float64) float64 {
    return defaultCalculator.Add(a, b)
}

// Subtract is a package-level convenience function
func Subtract(a, b float64) float64 {
    return defaultCalculator.Subtract(a, b)
}
// File: math/constants.go
package math

// Mathematical constants
const (
    Pi  = 3.14159265358979323846
    E   = 2.71828182845904523536
    Phi = 1.61803398874989484820 // Golden ratio
)

// Conversion factors
const (
    DegreesToRadians = Pi / 180
    RadiansToDegrees = 180 / Pi
)

// Common values
var (
    // Sqrt2 is the square root of 2
    Sqrt2 = 1.41421356237309504880
    // Sqrt3 is the square root of 3
    Sqrt3 = 1.73205080756887729352
)
// File: math/geometry.go
package math

import "math"

// Shape interface defines common shape operations
type Shape interface {
    Area() float64
    Perimeter() float64
}

// Circle represents a circle
type Circle struct {
    Radius float64
}

// Area calculates the area of a circle
func (c Circle) Area() float64 {
    return Pi * c.Radius * c.Radius
}

// Perimeter calculates the perimeter of a circle
func (c Circle) Perimeter() float64 {
    return 2 * Pi * c.Radius
}

// Rectangle represents a rectangle
type Rectangle struct {
    Width, Height float64
}

// Area calculates the area of a rectangle
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// Perimeter calculates the perimeter of a rectangle
func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

// Triangle represents a triangle
type Triangle struct {
    A, B, C float64 // side lengths
}

// Area calculates the area using Heron's formula
func (t Triangle) Area() float64 {
    s := (t.A + t.B + t.C) / 2 // semi-perimeter
    return math.Sqrt(s * (s - t.A) * (s - t.B) * (s - t.C))
}

// Perimeter calculates the perimeter of a triangle
func (t Triangle) Perimeter() float64 {
    return t.A + t.B + t.C
}

// CalculateArea is a utility function for any shape
func CalculateArea(shape Shape) float64 {
    return shape.Area()
}

// CalculatePerimeter is a utility function for any shape
func CalculatePerimeter(shape Shape) float64 {
    return shape.Perimeter()
}
// File: main.go
package main

import (
    "fmt"
    "math" // local math package
)

func main() {
    fmt.Println("=== Go Packages Demonstration ===")
    
    // Using package-level functions
    fmt.Println("\n--- Package-level Functions ---")
    fmt.Printf("5 + 3 = %.2f\n", math.Add(5, 3))
    fmt.Printf("5 - 3 = %.2f\n", math.Subtract(5, 3))
    
    // Using package constants
    fmt.Println("\n--- Package Constants ---")
    fmt.Printf("Pi = %.10f\n", math.Pi)
    fmt.Printf("E = %.10f\n", math.E)
    fmt.Printf("Golden Ratio = %.10f\n", math.Phi)
    
    // Creating and using calculator instance
    fmt.Println("\n--- Calculator Instance ---")
    calc := math.NewCalculator(3) // 3 decimal places
    fmt.Printf("Calculator precision: %d\n", calc.GetPrecision())
    
    result, err := calc.Divide(22, 7)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
    } else {
        fmt.Printf("22 / 7 = %.3f\n", result)
    }
    
    // Using geometry shapes
    fmt.Println("\n--- Geometry Shapes ---")
    
    circle := math.Circle{Radius: 5}
    rectangle := math.Rectangle{Width: 4, Height: 6}
    triangle := math.Triangle{A: 3, B: 4, C: 5}
    
    shapes := []math.Shape{circle, rectangle, triangle}
    shapeNames := []string{"Circle", "Rectangle", "Triangle"}
    
    for i, shape := range shapes {
        fmt.Printf("%s:\n", shapeNames[i])
        fmt.Printf("  Area: %.2f\n", math.CalculateArea(shape))
        fmt.Printf("  Perimeter: %.2f\n", math.CalculatePerimeter(shape))
    }
    
    // Demonstrating type assertion with shapes
    fmt.Println("\n--- Shape Type Information ---")
    for i, shape := range shapes {
        fmt.Printf("%s details:\n", shapeNames[i])
        
        switch s := shape.(type) {
        case math.Circle:
            fmt.Printf("  Type: Circle, Radius: %.2f\n", s.Radius)
        case math.Rectangle:
            fmt.Printf("  Type: Rectangle, Width: %.2f, Height: %.2f\n", 
                s.Width, s.Height)
        case math.Triangle:
            fmt.Printf("  Type: Triangle, Sides: %.2f, %.2f, %.2f\n", 
                s.A, s.B, s.C)
        }
    }
}

Explanation:

  • Lines 1-3: Package declaration for the math package
  • Lines 6-12: Exported struct with constructor function
  • Lines 46-50: Unexported helper function (lowercase name)
  • Lines 55-61: Package-level convenience functions using default instance
  • Different files in same package share namespace

Example 2: Go Modules and Dependency Management

// File: go.mod (created with: go mod init example.com/myproject)
module example.com/myproject

go 1.21

require (
    github.com/gorilla/mux v1.8.0
    github.com/joho/godotenv v1.4.0
    golang.org/x/crypto v0.14.0
)

require (
    golang.org/x/sys v0.13.0 // indirect
    golang.org/x/term v0.13.0 // indirect
)
// File: config/config.go
package config

import (
    "fmt"
    "os"
    "strconv"
    "time"
    
    "github.com/joho/godotenv"
)

// Config holds application configuration
type Config struct {
    Port         int
    DatabaseURL  string
    Debug        bool
    Timeout      time.Duration
    APIKey       string
}

// Default configuration values
const (
    DefaultPort    = 8080
    DefaultTimeout = 30 * time.Second
)

// Load loads configuration from environment variables
func Load() (*Config, error) {
    // Try to load .env file (ignore error if not present)
    _ = godotenv.Load()
    
    config := &Config{
        Port:    DefaultPort,
        Timeout: DefaultTimeout,
    }
    
    // Parse port
    if portStr := os.Getenv("PORT"); portStr != "" {
        port, err := strconv.Atoi(portStr)
        if err != nil {
            return nil, fmt.Errorf("invalid PORT value: %w", err)
        }
        config.Port = port
    }
    
    // Parse database URL (required)
    config.DatabaseURL = os.Getenv("DATABASE_URL")
    if config.DatabaseURL == "" {
        return nil, fmt.Errorf("DATABASE_URL environment variable is required")
    }
    
    // Parse debug flag
    config.Debug = os.Getenv("DEBUG") == "true"
    
    // Parse timeout
    if timeoutStr := os.Getenv("TIMEOUT"); timeoutStr != "" {
        timeout, err := time.ParseDuration(timeoutStr)
        if err != nil {
            return nil, fmt.Errorf("invalid TIMEOUT value: %w", err)
        }
        config.Timeout = timeout
    }
    
    // API key (optional)
    config.APIKey = os.Getenv("API_KEY")
    
    return config, nil
}

// Validate validates the configuration
func (c *Config) Validate() error {
    if c.Port <= 0 || c.Port > 65535 {
        return fmt.Errorf("port must be between 1 and 65535, got %d", c.Port)
    }
    
    if c.Timeout <= 0 {
        return fmt.Errorf("timeout must be positive, got %v", c.Timeout)
    }
    
    if c.DatabaseURL == "" {
        return fmt.Errorf("database URL cannot be empty")
    }
    
    return nil
}

// String returns a string representation of the config
func (c *Config) String() string {
    return fmt.Sprintf("Config{Port: %d, Debug: %t, Timeout: %v, DatabaseURL: %s, HasAPIKey: %t}",
        c.Port, c.Debug, c.Timeout, maskURL(c.DatabaseURL), c.APIKey != "")
}

// maskURL masks sensitive parts of a URL for logging
func maskURL(url string) string {
    if len(url) <= 10 {
        return "***"
    }
    return url[:10] + "***"
}
// File: server/handler.go
package server

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "time"
    
    "github.com/gorilla/mux"
    "example.com/myproject/config"
)

// Server represents the HTTP server
type Server struct {
    config *config.Config
    router *mux.Router
}

// New creates a new server instance
func New(cfg *config.Config) *Server {
    s := &Server{
        config: cfg,
        router: mux.NewRouter(),
    }
    
    s.setupRoutes()
    return s
}

// setupRoutes configures the server routes
func (s *Server) setupRoutes() {
    // API routes
    api := s.router.PathPrefix("/api").Subrouter()
    api.HandleFunc("/health", s.handleHealth).Methods("GET")
    api.HandleFunc("/config", s.handleConfig).Methods("GET")
    api.HandleFunc("/time", s.handleTime).Methods("GET")
    
    // Middleware
    s.router.Use(s.loggingMiddleware)
    if s.config.Debug {
        s.router.Use(s.debugMiddleware)
    }
}

// Start starts the HTTP server
func (s *Server) Start() error {
    addr := fmt.Sprintf(":%d", s.config.Port)
    log.Printf("Starting server on %s", addr)
    
    srv := &http.Server{
        Addr:         addr,
        Handler:      s.router,
        ReadTimeout:  s.config.Timeout,
        WriteTimeout: s.config.Timeout,
        IdleTimeout:  s.config.Timeout * 2,
    }
    
    return srv.ListenAndServe()
}

// Health check endpoint
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
    response := map[string]interface{}{
        "status":    "healthy",
        "timestamp": time.Now().Unix(),
        "debug":     s.config.Debug,
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(response)
}

// Config endpoint (for debugging)
func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
    if !s.config.Debug {
        http.Error(w, "Debug mode required", http.StatusForbidden)
        return
    }
    
    response := map[string]interface{}{
        "port":    s.config.Port,
        "debug":   s.config.Debug,
        "timeout": s.config.Timeout.String(),
        "hasKey":  s.config.APIKey != "",
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(response)
}

// Time endpoint
func (s *Server) handleTime(w http.ResponseWriter, r *http.Request) {
    now := time.Now()
    response := map[string]interface{}{
        "unix":      now.Unix(),
        "rfc3339":   now.Format(time.RFC3339),
        "formatted": now.Format("2006-01-02 15:04:05"),
        "timezone":  now.Location().String(),
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(response)
}

// Logging middleware
func (s *Server) loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        
        // Create a response writer that captures status code
        wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
        
        next.ServeHTTP(wrapped, r)
        
        duration := time.Since(start)
        log.Printf("%s %s %d %v", r.Method, r.URL.Path, wrapped.statusCode, duration)
    })
}

// Debug middleware
func (s *Server) debugMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Printf("DEBUG: %s %s from %s", r.Method, r.URL.String(), r.RemoteAddr)
        log.Printf("DEBUG: Headers: %v", r.Header)
        
        next.ServeHTTP(w, r)
    })
}

// responseWriter wraps http.ResponseWriter to capture status code
type responseWriter struct {
    http.ResponseWriter
    statusCode int
}

func (rw *responseWriter) WriteHeader(code int) {
    rw.statusCode = code
    rw.ResponseWriter.WriteHeader(code)
}
// File: main.go
package main

import (
    "log"
    "os"
    
    "example.com/myproject/config"
    "example.com/myproject/server"
)

func main() {
    log.Println("Starting application...")
    
    // Load configuration
    cfg, err := config.Load()
    if err != nil {
        log.Fatalf("Failed to load configuration: %v", err)
    }
    
    // Validate configuration
    if err := cfg.Validate(); err != nil {
        log.Fatalf("Invalid configuration: %v", err)
    }
    
    log.Printf("Configuration loaded: %s", cfg)
    
    // Create and start server
    srv := server.New(cfg)
    
    log.Printf("Starting server on port %d...", cfg.Port)
    if err := srv.Start(); err != nil {
        log.Fatalf("Server failed: %v", err)
    }
}

Example 3: Package Initialization and Testing

// File: database/connection.go
package database

import (
    "database/sql"
    "fmt"
    "log"
    "sync"
    "time"
    
    _ "github.com/lib/pq" // PostgreSQL driver
)

var (
    // Package-level database connection
    defaultDB *sql.DB
    // Ensure initialization happens only once
    once sync.Once
    // Connection configuration
    config ConnectionConfig
)

// ConnectionConfig holds database connection parameters
type ConnectionConfig struct {
    Host     string
    Port     int
    Database string
    Username string
    Password string
    SSLMode  string
    MaxConns int
    Timeout  time.Duration
}

// DefaultConfig returns default connection configuration
func DefaultConfig() ConnectionConfig {
    return ConnectionConfig{
        Host:     "localhost",
        Port:     5432,
        Database: "myapp",
        Username: "postgres",
        Password: "",
        SSLMode:  "disable",
        MaxConns: 10,
        Timeout:  30 * time.Second,
    }
}

// Initialize sets up the database connection
func Initialize(cfg ConnectionConfig) error {
    var err error
    
    once.Do(func() {
        config = cfg
        
        // Build connection string
        connStr := fmt.Sprintf(
            "host=%s port=%d dbname=%s user=%s password=%s sslmode=%s",
            cfg.Host, cfg.Port, cfg.Database, cfg.Username, cfg.Password, cfg.SSLMode,
        )
        
        // Open database connection
        defaultDB, err = sql.Open("postgres", connStr)
        if err != nil {
            return
        }
        
        // Configure connection pool
        defaultDB.SetMaxOpenConns(cfg.MaxConns)
        defaultDB.SetMaxIdleConns(cfg.MaxConns / 2)
        defaultDB.SetConnMaxLifetime(time.Hour)
        
        // Test connection
        err = defaultDB.Ping()
        if err != nil {
            defaultDB.Close()
            defaultDB = nil
            return
        }
        
        log.Printf("Database connection established: %s:%d/%s", 
            cfg.Host, cfg.Port, cfg.Database)
    })
    
    return err
}

// GetDB returns the default database connection
func GetDB() *sql.DB {
    if defaultDB == nil {
        log.Fatal("Database not initialized. Call Initialize() first.")
    }
    return defaultDB
}

// Close closes the database connection
func Close() error {
    if defaultDB != nil {
        return defaultDB.Close()
    }
    return nil
}

// Health checks database connectivity
func Health() error {
    if defaultDB == nil {
        return fmt.Errorf("database not initialized")
    }
    return defaultDB.Ping()
}

// Transaction helper
type TxFunc func(*sql.Tx) error

// WithTransaction executes a function within a database transaction
func WithTransaction(fn TxFunc) error {
    db := GetDB()
    
    tx, err := db.Begin()
    if err != nil {
        return fmt.Errorf("begin transaction: %w", err)
    }
    
    defer func() {
        if p := recover(); p != nil {
            tx.Rollback()
            panic(p) // Re-throw panic after rollback
        } else if err != nil {
            tx.Rollback()
        } else {
            err = tx.Commit()
        }
    }()
    
    err = fn(tx)
    return err
}
// File: database/models.go
package database

import (
    "database/sql"
    "fmt"
    "time"
)

// User represents a user in the database
type User struct {
    ID        int64     `json:"id"`
    Username  string    `json:"username"`
    Email     string    `json:"email"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}

// UserRepository handles user database operations
type UserRepository struct {
    db *sql.DB
}

// NewUserRepository creates a new user repository
func NewUserRepository() *UserRepository {
    return &UserRepository{db: GetDB()}
}

// Create creates a new user
func (r *UserRepository) Create(user *User) error {
    query := `
        INSERT INTO users (username, email, created_at, updated_at)
        VALUES ($1, $2, $3, $4)
        RETURNING id`
    
    now := time.Now()
    user.CreatedAt = now
    user.UpdatedAt = now
    
    err := r.db.QueryRow(query, user.Username, user.Email, user.CreatedAt, user.UpdatedAt).
        Scan(&user.ID)
    
    if err != nil {
        return fmt.Errorf("create user: %w", err)
    }
    
    return nil
}

// GetByID retrieves a user by ID
func (r *UserRepository) GetByID(id int64) (*User, error) {
    query := `
        SELECT id, username, email, created_at, updated_at
        FROM users
        WHERE id = $1`
    
    user := &User{}
    err := r.db.QueryRow(query, id).Scan(
        &user.ID, &user.Username, &user.Email,
        &user.CreatedAt, &user.UpdatedAt,
    )
    
    if err != nil {
        if err == sql.ErrNoRows {
            return nil, fmt.Errorf("user not found: %d", id)
        }
        return nil, fmt.Errorf("get user by ID: %w", err)
    }
    
    return user, nil
}

// GetByUsername retrieves a user by username
func (r *UserRepository) GetByUsername(username string) (*User, error) {
    query := `
        SELECT id, username, email, created_at, updated_at
        FROM users
        WHERE username = $1`
    
    user := &User{}
    err := r.db.QueryRow(query, username).Scan(
        &user.ID, &user.Username, &user.Email,
        &user.CreatedAt, &user.UpdatedAt,
    )
    
    if err != nil {
        if err == sql.ErrNoRows {
            return nil, fmt.Errorf("user not found: %s", username)
        }
        return nil, fmt.Errorf("get user by username: %w", err)
    }
    
    return user, nil
}

// Update updates a user
func (r *UserRepository) Update(user *User) error {
    query := `
        UPDATE users
        SET username = $2, email = $3, updated_at = $4
        WHERE id = $1`
    
    user.UpdatedAt = time.Now()
    
    result, err := r.db.Exec(query, user.ID, user.Username, user.Email, user.UpdatedAt)
    if err != nil {
        return fmt.Errorf("update user: %w", err)
    }
    
    rowsAffected, err := result.RowsAffected()
    if err != nil {
        return fmt.Errorf("get rows affected: %w", err)
    }
    
    if rowsAffected == 0 {
        return fmt.Errorf("user not found: %d", user.ID)
    }
    
    return nil
}

// Delete deletes a user
func (r *UserRepository) Delete(id int64) error {
    query := `DELETE FROM users WHERE id = $1`
    
    result, err := r.db.Exec(query, id)
    if err != nil {
        return fmt.Errorf("delete user: %w", err)
    }
    
    rowsAffected, err := result.RowsAffected()
    if err != nil {
        return fmt.Errorf("get rows affected: %w", err)
    }
    
    if rowsAffected == 0 {
        return fmt.Errorf("user not found: %d", id)
    }
    
    return nil
}

// List retrieves all users with pagination
func (r *UserRepository) List(limit, offset int) ([]*User, error) {
    query := `
        SELECT id, username, email, created_at, updated_at
        FROM users
        ORDER BY created_at DESC
        LIMIT $1 OFFSET $2`
    
    rows, err := r.db.Query(query, limit, offset)
    if err != nil {
        return nil, fmt.Errorf("list users: %w", err)
    }
    defer rows.Close()
    
    var users []*User
    
    for rows.Next() {
        user := &User{}
        err := rows.Scan(
            &user.ID, &user.Username, &user.Email,
            &user.CreatedAt, &user.UpdatedAt,
        )
        if err != nil {
            return nil, fmt.Errorf("scan user: %w", err)
        }
        users = append(users, user)
    }
    
    if err = rows.Err(); err != nil {
        return nil, fmt.Errorf("rows error: %w", err)
    }
    
    return users, nil
}
// File: database/database_test.go
package database

import (
    "testing"
    "time"
)

func TestDefaultConfig(t *testing.T) {
    cfg := DefaultConfig()
    
    if cfg.Host != "localhost" {
        t.Errorf("Expected host 'localhost', got '%s'", cfg.Host)
    }
    
    if cfg.Port != 5432 {
        t.Errorf("Expected port 5432, got %d", cfg.Port)
    }
    
    if cfg.MaxConns != 10 {
        t.Errorf("Expected MaxConns 10, got %d", cfg.MaxConns)
    }
    
    if cfg.Timeout != 30*time.Second {
        t.Errorf("Expected timeout 30s, got %v", cfg.Timeout)
    }
}

func TestConnectionConfig(t *testing.T) {
    cfg := ConnectionConfig{
        Host:     "testhost",
        Port:     1234,
        Database: "testdb",
        Username: "testuser",
        Password: "testpass",
        SSLMode:  "require",
        MaxConns: 5,
        Timeout:  10 * time.Second,
    }
    
    if cfg.Host != "testhost" {
        t.Errorf("Expected host 'testhost', got '%s'", cfg.Host)
    }
    
    if cfg.Port != 1234 {
        t.Errorf("Expected port 1234, got %d", cfg.Port)
    }
}

// Note: These tests would require a test database for full testing
// In practice, you'd use dependency injection or test databases

Example 4: Publishing and Consuming Packages

// File: utils/stringutil.go (example publishable package)
// Package stringutil provides utility functions for string manipulation
package stringutil

import (
    "regexp"
    "strings"
    "unicode"
)

// Reverse returns the reverse of a string
func Reverse(s string) string {
    runes := []rune(s)
    for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
        runes[i], runes[j] = runes[j], runes[i]
    }
    return string(runes)
}

// IsPalindrome checks if a string is a palindrome (ignoring case and spaces)
func IsPalindrome(s string) bool {
    // Clean the string: remove spaces and convert to lowercase
    cleaned := strings.ToLower(strings.ReplaceAll(s, " ", ""))
    return cleaned == Reverse(cleaned)
}

// ToSnakeCase converts a string to snake_case
func ToSnakeCase(s string) string {
    var result strings.Builder
    
    for i, r := range s {
        if unicode.IsUpper(r) {
            if i > 0 {
                result.WriteRune('_')
            }
            result.WriteRune(unicode.ToLower(r))
        } else {
            result.WriteRune(r)
        }
    }
    
    return result.String()
}

// ToCamelCase converts a string to camelCase
func ToCamelCase(s string) string {
    words := strings.Fields(strings.ReplaceAll(s, "_", " "))
    if len(words) == 0 {
        return ""
    }
    
    var result strings.Builder
    result.WriteString(strings.ToLower(words[0]))
    
    for _, word := range words[1:] {
        if len(word) > 0 {
            result.WriteRune(unicode.ToUpper(rune(word[0])))
            result.WriteString(strings.ToLower(word[1:]))
        }
    }
    
    return result.String()
}

// ToPascalCase converts a string to PascalCase
func ToPascalCase(s string) string {
    words := strings.Fields(strings.ReplaceAll(s, "_", " "))
    var result strings.Builder
    
    for _, word := range words {
        if len(word) > 0 {
            result.WriteRune(unicode.ToUpper(rune(word[0])))
            result.WriteString(strings.ToLower(word[1:]))
        }
    }
    
    return result.String()
}

// IsValidEmail checks if a string is a valid email format
func IsValidEmail(email string) bool {
    emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
    return emailRegex.MatchString(email)
}

// Truncate truncates a string to a specified length
func Truncate(s string, length int) string {
    if length <= 0 {
        return ""
    }
    
    runes := []rune(s)
    if len(runes) <= length {
        return s
    }
    
    if length <= 3 {
        return string(runes[:length])
    }
    
    return string(runes[:length-3]) + "..."
}

// WordCount returns the number of words in a string
func WordCount(s string) int {
    fields := strings.Fields(s)
    return len(fields)
}

// ContainsAny checks if a string contains any of the specified substrings
func ContainsAny(s string, substrings ...string) bool {
    for _, substring := range substrings {
        if strings.Contains(s, substring) {
            return true
        }
    }
    return false
}

// RemoveAccents removes accents from characters (basic implementation)
func RemoveAccents(s string) string {
    replacements := map[rune]rune{
        'á': 'a', 'à': 'a', 'ä': 'a', 'â': 'a', 'ā': 'a', 'ã': 'a',
        'Á': 'A', 'À': 'A', 'Ä': 'A', 'Â': 'A', 'Ā': 'A', 'Ã': 'A',
        'é': 'e', 'è': 'e', 'ë': 'e', 'ê': 'e', 'ē': 'e',
        'É': 'E', 'È': 'E', 'Ë': 'E', 'Ê': 'E', 'Ē': 'E',
        'í': 'i', 'ì': 'i', 'ï': 'i', 'î': 'i', 'ī': 'i',
        'Í': 'I', 'Ì': 'I', 'Ï': 'I', 'Î': 'I', 'Ī': 'I',
        'ó': 'o', 'ò': 'o', 'ö': 'o', 'ô': 'o', 'ō': 'o', 'õ': 'o',
        'Ó': 'O', 'Ò': 'O', 'Ö': 'O', 'Ô': 'O', 'Ō': 'O', 'Õ': 'O',
        'ú': 'u', 'ù': 'u', 'ü': 'u', 'û': 'u', 'ū': 'u',
        'Ú': 'U', 'Ù': 'U', 'Ü': 'U', 'Û': 'U', 'Ū': 'U',
        'ñ': 'n', 'Ñ': 'N',
        'ç': 'c', 'Ç': 'C',
    }
    
    var result strings.Builder
    for _, r := range s {
        if replacement, exists := replacements[r]; exists {
            result.WriteRune(replacement)
        } else {
            result.WriteRune(r)
        }
    }
    
    return result.String()
}
// File: utils/stringutil_test.go
package stringutil

import "testing"

func TestReverse(t *testing.T) {
    tests := []struct {
        input    string
        expected string
    }{
        {"hello", "olleh"},
        {"", ""},
        {"a", "a"},
        {"Go", "oG"},
        {"🚀", "🚀"},
        {"hello 🌍", "🌍 olleh"},
    }
    
    for _, test := range tests {
        result := Reverse(test.input)
        if result != test.expected {
            t.Errorf("Reverse(%q) = %q, expected %q", test.input, result, test.expected)
        }
    }
}

func TestIsPalindrome(t *testing.T) {
    tests := []struct {
        input    string
        expected bool
    }{
        {"racecar", true},
        {"A man a plan a canal Panama", true},
        {"race a car", false},
        {"hello", false},
        {"", true},
        {"a", true},
        {"Aa", true},
    }
    
    for _, test := range tests {
        result := IsPalindrome(test.input)
        if result != test.expected {
            t.Errorf("IsPalindrome(%q) = %t, expected %t", test.input, result, test.expected)
        }
    }
}

func TestToSnakeCase(t *testing.T) {
    tests := []struct {
        input    string
        expected string
    }{
        {"HelloWorld", "hello_world"},
        {"XMLHttpRequest", "x_m_l_http_request"},
        {"iPhone", "i_phone"},
        {"testString", "test_string"},
        {"HTML", "h_t_m_l"},
        {"", ""},
        {"a", "a"},
    }
    
    for _, test := range tests {
        result := ToSnakeCase(test.input)
        if result != test.expected {
            t.Errorf("ToSnakeCase(%q) = %q, expected %q", test.input, result, test.expected)
        }
    }
}

func TestToCamelCase(t *testing.T) {
    tests := []struct {
        input    string
        expected string
    }{
        {"hello_world", "helloWorld"},
        {"test string", "testString"},
        {"XML_HTTP_Request", "xmlHttpRequest"},
        {"", ""},
        {"single", "single"},
        {"multiple word string", "multipleWordString"},
    }
    
    for _, test := range tests {
        result := ToCamelCase(test.input)
        if result != test.expected {
            t.Errorf("ToCamelCase(%q) = %q, expected %q", test.input, result, test.expected)
        }
    }
}

func TestIsValidEmail(t *testing.T) {
    tests := []struct {
        input    string
        expected bool
    }{
        {"test@example.com", true},
        {"user.name@domain.co.uk", true},
        {"invalid.email", false},
        {"@domain.com", false},
        {"user@", false},
        {"", false},
        {"user@domain", false},
        {"user name@domain.com", false},
    }
    
    for _, test := range tests {
        result := IsValidEmail(test.input)
        if result != test.expected {
            t.Errorf("IsValidEmail(%q) = %t, expected %t", test.input, result, test.expected)
        }
    }
}

func TestTruncate(t *testing.T) {
    tests := []struct {
        input    string
        length   int
        expected string
    }{
        {"hello world", 5, "he..."},
        {"short", 10, "short"},
        {"exact", 5, "exact"},
        {"", 5, ""},
        {"test", 0, ""},
        {"hi", 2, "hi"},
        {"hello", 3, "hel"},
        {"testing truncation", 8, "testi..."},
    }
    
    for _, test := range tests {
        result := Truncate(test.input, test.length)
        if result != test.expected {
            t.Errorf("Truncate(%q, %d) = %q, expected %q", 
                test.input, test.length, result, test.expected)
        }
    }
}

func BenchmarkReverse(b *testing.B) {
    s := "Hello, World! This is a test string for benchmarking."
    
    for i := 0; i < b.N; i++ {
        Reverse(s)
    }
}

func BenchmarkToSnakeCase(b *testing.B) {
    s := "ThisIsATestStringForBenchmarkingSnakeCaseConversion"
    
    for i := 0; i < b.N; i++ {
        ToSnakeCase(s)
    }
}

6. Hands-On Practice (30-40 minutes)

Exercise 1: Create a Logger Package (10 minutes)

Objective: Build a configurable logging package with different levels and outputs

Instructions:

  1. Create a logger package with different log levels (DEBUG, INFO, WARN, ERROR)
  2. Support multiple output destinations (console, file, both)
  3. Include timestamp and caller information
  4. Make it thread-safe for concurrent use

Starter Code:

package logger

// TODO: Define log levels
// TODO: Create Logger struct
// TODO: Implement New() constructor
// TODO: Add level-specific methods (Debug, Info, Warn, Error)
// TODO: Support different outputs

Exercise 2: HTTP Client Package with Middleware (15 minutes)

Objective: Create a reusable HTTP client package with middleware support

Requirements:

  • Support for different HTTP methods (GET, POST, PUT, DELETE)
  • Middleware for authentication, logging, retry logic
  • Timeout configuration
  • Response parsing utilities
  • Error handling with custom error types

Instructions:

  1. Design client interface and implementation
  2. Create middleware chain pattern
  3. Add configuration options
  4. Include comprehensive error handling
  5. Write tests for main functionality

Exercise 3: Configuration Management Package (15 minutes)

Objective: Build a comprehensive configuration package for applications

Challenge: Create a package that can load configuration from multiple sources

Requirements:

  • Support environment variables, JSON files, YAML files
  • Configuration validation and defaults
  • Hot-reloading of configuration files
  • Type-safe configuration structs
  • Configuration merging from multiple sources

Extension Ideas:

  • Add configuration encryption/decryption
  • Support remote configuration sources (etcd, consul)
  • Add configuration change notifications
  • Create configuration migration utilities

7. Common Pitfalls and Troubleshooting (10 minutes)

Frequent Mistakes

  1. Problem: Circular dependencies between packages Cause: Poor package design with interdependent functionality Solution: Use dependency inversion and interface segregation

  2. Problem: Overly granular packages with single functions Cause: Misunderstanding package purpose and scope Solution: Group related functionality logically in packages

  3. Problem: Version conflicts and dependency hell Cause: Not using semantic versioning or proper module management Solution: Use Go modules with proper versioning practices

  4. Problem: Package initialization order issues Cause: Relying on init() functions with complex dependencies Solution: Use explicit initialization functions instead of init()

Debugging Tips

  • Dependency Analysis: Use go mod graph to visualize dependencies
  • Build Flags: Use -v flag to see package compilation order
  • Module Commands: Use go list -m all to see all module versions
  • Circular Dependencies: Use go mod tidy and dependency analysis tools

8. Best Practices and Professional Tips (5 minutes)

Industry Standards

  • Package Naming: Short, concise, descriptive names
  • API Design: Minimal, focused interfaces
  • Version Management: Semantic versioning with proper changelogs
  • Documentation: Package-level docs with examples

Code Organization

// Good: Clear package structure
myproject/
├── cmd/           // Main applications
├── internal/      // Private packages
├── pkg/          // Public packages
├── api/          // API definitions
├── configs/      // Configuration files
├── deployments/  // Deployment configs
└── docs/         // Documentation

Performance Considerations

  • Package Size: Keep packages focused but not overly granular
  • Initialization: Minimize work in init() functions
  • Dependencies: Prefer smaller, focused dependencies
  • Build Times: Consider package dependency graph impact

Next Steps in Your Learning Journey

  • Microservices: Package design for distributed systems
  • Plugin Architecture: Using packages for extensible systems
  • Code Generation: Using packages with go generate
  • Advanced Modules: Replace directives, vendor management

9. Summary and Review (5 minutes)

Key Takeaways

  1. Packages: Fundamental unit of code organization and reuse
  2. Modules: Modern dependency management with versioning
  3. Visibility: Use exported/unexported names for proper encapsulation
  4. Design: Focus on clear APIs and minimal dependencies

Quick Review Questions

  1. What’s the difference between a package and a module in Go?
  2. How do you make a function or type visible outside its package?
  3. What file defines a Go module and its dependencies?
  4. When should you use init() functions vs explicit initialization?

Answers:

  1. Package is a collection of Go files; module is a collection of packages with versioning
  2. Start the name with an uppercase letter (exported names)
  3. go.mod file defines modules and dependencies
  4. Use explicit initialization for complex setup; init() for simple package-level setup

For Instructors

Teaching Tips

  • Visual Organization: Use directory trees to show package structure
  • Real Examples: Show popular Go projects’ package organization
  • Hands-on Modules: Have students create and publish a simple module
  • Dependency Management: Demonstrate common dependency scenarios

Assessment Ideas

  • Package Design: Design packages for a real-world application
  • Module Management: Handle version upgrades and conflicts
  • API Design: Create clean, minimal package interfaces
  • Code Organization: Refactor monolithic code into packages

Adaptation Notes

  • Beginner Focus: Start with single packages, progress to modules
  • Advanced Students: Cover replace directives, vendor management
  • Team Projects: Use packages for collaborative development
  • Time Management: Package concepts build incrementally