Go Frameworks and Libraries

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

  • Build high-performance web applications using Gin and Echo frameworks
  • Implement database operations with GORM ORM
  • Integrate popular Go libraries for logging, validation, and authentication
  • Choose appropriate tools for different project requirements
  • Build complete applications using modern Go ecosystem
  • Deploy and monitor Go applications in production

1. Welcome and Introduction (5 minutes)

Icebreaker Activity

While Go’s standard library is powerful, the ecosystem has evolved to include excellent frameworks and libraries that accelerate development while maintaining Go’s performance characteristics. Today we’ll explore the most popular and production-ready tools that make Go development even more productive.

Lesson Overview

  • Duration: 3-4 days (8-10 hours)
  • Prerequisites: Go fundamentals, web development basics, database knowledge
  • Materials Needed: Go development environment, Docker for databases

2. Learning Objectives and Session Outline (5 minutes)

What We’ll Cover Today:

  1. Gin Framework - High-performance HTTP web framework
  2. Echo Framework - Minimalist, fast web framework
  3. GORM - Developer-friendly ORM for database operations
  4. Popular Libraries - Logging, validation, configuration, and more
  5. Authentication & Authorization - JWT, middleware, and security
  6. Project Structure - Best practices for organizing Go applications
  7. Deployment - Building and deploying production applications

Success Criteria:

By the end of this session, you should be able to build and deploy a complete, production-ready web application using Go’s ecosystem.

3. Foundation Knowledge (15-20 minutes)

Core Concepts

Framework vs Library Philosophy

Go’s ecosystem follows the philosophy of providing focused, composable tools rather than monolithic frameworks. This gives developers the flexibility to choose exactly what they need.

Real-World Analogy: Think of Go frameworks like a toolbox where each tool has a specific purpose, versus a Swiss Army knife that tries to do everything.

Why This Matters: This approach leads to smaller binaries, better performance, and more maintainable code since you only include what you actually use.

  • Web Frameworks: Gin, Echo, Fiber, Chi
  • ORMs: GORM, Ent, SQLBoiler
  • Logging: Logrus, Zap, Slog
  • Configuration: Viper, Cobra
  • Testing: Testify, GoMock
  • Validation: Validator, Ozzo-validation

Key Terminology

  • Middleware: Functions that execute before or after HTTP handlers
  • ORM: Object-Relational Mapping for database operations
  • Dependency Injection: Pattern for managing application dependencies
  • Router: Component that matches URLs to handlers
  • Binding: Converting HTTP request data to Go structs

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

Understanding Gin Framework

Gin is a high-performance HTTP web framework with a martini-like API but much better performance and lower memory footprint.

Basic Gin Application

package main

import (
    "net/http"
    "strconv"
    
    "github.com/gin-gonic/gin"
)

// User represents a user in our system
type User struct {
    ID       int    `json:"id" gorm:"primaryKey"`
    Username string `json:"username" gorm:"uniqueIndex" binding:"required,min=3,max=20"`
    Email    string `json:"email" gorm:"uniqueIndex" binding:"required,email"`
    FullName string `json:"full_name" binding:"required"`
    Age      int    `json:"age" binding:"gte=0,lte=130"`
}

// Response represents a standard API response
type Response struct {
    Success bool        `json:"success"`
    Message string      `json:"message,omitempty"`
    Data    interface{} `json:"data,omitempty"`
    Error   string      `json:"error,omitempty"`
}

// UserService handles user business logic
type UserService struct {
    users  map[int]*User
    nextID int
}

func NewUserService() *UserService {
    return &UserService{
        users:  make(map[int]*User),
        nextID: 1,
    }
}

func (s *UserService) CreateUser(user *User) error {
    // Check if username exists
    for _, existingUser := range s.users {
        if existingUser.Username == user.Username {
            return errors.New("username already exists")
        }
        if existingUser.Email == user.Email {
            return errors.New("email already exists")
        }
    }
    
    user.ID = s.nextID
    s.nextID++
    s.users[user.ID] = user
    return nil
}

func (s *UserService) GetUser(id int) (*User, error) {
    user, exists := s.users[id]
    if !exists {
        return nil, errors.New("user not found")
    }
    return user, nil
}

func (s *UserService) GetAllUsers() []*User {
    users := make([]*User, 0, len(s.users))
    for _, user := range s.users {
        users = append(users, user)
    }
    return users
}

func (s *UserService) UpdateUser(id int, updatedUser *User) error {
    user, exists := s.users[id]
    if !exists {
        return errors.New("user not found")
    }
    
    // Check for duplicate username/email (excluding current user)
    for userID, existingUser := range s.users {
        if userID != id {
            if existingUser.Username == updatedUser.Username {
                return errors.New("username already exists")
            }
            if existingUser.Email == updatedUser.Email {
                return errors.New("email already exists")
            }
        }
    }
    
    updatedUser.ID = id
    s.users[id] = updatedUser
    return nil
}

func (s *UserService) DeleteUser(id int) error {
    _, exists := s.users[id]
    if !exists {
        return errors.New("user not found")
    }
    delete(s.users, id)
    return nil
}

// UserController handles HTTP requests for users
type UserController struct {
    service *UserService
}

func NewUserController(service *UserService) *UserController {
    return &UserController{service: service}
}

func (c *UserController) CreateUser(ctx *gin.Context) {
    var user User
    
    // Bind and validate JSON input
    if err := ctx.ShouldBindJSON(&user); err != nil {
        ctx.JSON(http.StatusBadRequest, Response{
            Success: false,
            Error:   err.Error(),
        })
        return
    }
    
    // Create user through service
    if err := c.service.CreateUser(&user); err != nil {
        status := http.StatusInternalServerError
        if err.Error() == "username already exists" || err.Error() == "email already exists" {
            status = http.StatusConflict
        }
        
        ctx.JSON(status, Response{
            Success: false,
            Error:   err.Error(),
        })
        return
    }
    
    ctx.JSON(http.StatusCreated, Response{
        Success: true,
        Message: "User created successfully",
        Data:    user,
    })
}

func (c *UserController) GetUser(ctx *gin.Context) {
    idStr := ctx.Param("id")
    id, err := strconv.Atoi(idStr)
    if err != nil {
        ctx.JSON(http.StatusBadRequest, Response{
            Success: false,
            Error:   "Invalid user ID",
        })
        return
    }
    
    user, err := c.service.GetUser(id)
    if err != nil {
        ctx.JSON(http.StatusNotFound, Response{
            Success: false,
            Error:   err.Error(),
        })
        return
    }
    
    ctx.JSON(http.StatusOK, Response{
        Success: true,
        Data:    user,
    })
}

func (c *UserController) GetAllUsers(ctx *gin.Context) {
    users := c.service.GetAllUsers()
    
    ctx.JSON(http.StatusOK, Response{
        Success: true,
        Data:    users,
    })
}

func (c *UserController) UpdateUser(ctx *gin.Context) {
    idStr := ctx.Param("id")
    id, err := strconv.Atoi(idStr)
    if err != nil {
        ctx.JSON(http.StatusBadRequest, Response{
            Success: false,
            Error:   "Invalid user ID",
        })
        return
    }
    
    var user User
    if err := ctx.ShouldBindJSON(&user); err != nil {
        ctx.JSON(http.StatusBadRequest, Response{
            Success: false,
            Error:   err.Error(),
        })
        return
    }
    
    if err := c.service.UpdateUser(id, &user); err != nil {
        status := http.StatusInternalServerError
        if err.Error() == "user not found" {
            status = http.StatusNotFound
        } else if strings.Contains(err.Error(), "already exists") {
            status = http.StatusConflict
        }
        
        ctx.JSON(status, Response{
            Success: false,
            Error:   err.Error(),
        })
        return
    }
    
    ctx.JSON(http.StatusOK, Response{
        Success: true,
        Message: "User updated successfully",
        Data:    user,
    })
}

func (c *UserController) DeleteUser(ctx *gin.Context) {
    idStr := ctx.Param("id")
    id, err := strconv.Atoi(idStr)
    if err != nil {
        ctx.JSON(http.StatusBadRequest, Response{
            Success: false,
            Error:   "Invalid user ID",
        })
        return
    }
    
    if err := c.service.DeleteUser(id); err != nil {
        ctx.JSON(http.StatusNotFound, Response{
            Success: false,
            Error:   err.Error(),
        })
        return
    }
    
    ctx.JSON(http.StatusOK, Response{
        Success: true,
        Message: "User deleted successfully",
    })
}

// Middleware for logging requests
func LoggerMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        
        // Process request
        c.Next()
        
        // Log details
        duration := time.Since(start)
        fmt.Printf("[%s] %s %s %d %v\n",
            start.Format("2006-01-02 15:04:05"),
            c.Request.Method,
            c.Request.URL.Path,
            c.Writer.Status(),
            duration,
        )
    }
}

// Middleware for handling CORS
func CORSMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
        c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
        c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
        c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")

        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(204)
            return
        }

        c.Next()
    }
}

// Error handling middleware
func ErrorHandlerMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            if err := recover(); err != nil {
                c.JSON(http.StatusInternalServerError, Response{
                    Success: false,
                    Error:   "Internal server error",
                })
                c.Abort()
            }
        }()
        c.Next()
    }
}

func main() {
    // Initialize service and controller
    userService := NewUserService()
    userController := NewUserController(userService)
    
    // Create Gin router
    r := gin.Default()
    
    // Add middleware
    r.Use(LoggerMiddleware())
    r.Use(CORSMiddleware())
    r.Use(ErrorHandlerMiddleware())
    
    // API routes
    api := r.Group("/api/v1")
    {
        users := api.Group("/users")
        {
            users.POST("", userController.CreateUser)
            users.GET("", userController.GetAllUsers)
            users.GET("/:id", userController.GetUser)
            users.PUT("/:id", userController.UpdateUser)
            users.DELETE("/:id", userController.DeleteUser)
        }
    }
    
    // Health check endpoint
    r.GET("/health", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "status": "healthy",
            "time":   time.Now().UTC(),
        })
    })
    
    // Start server
    fmt.Println("Server starting on port 8080...")
    r.Run(":8080")
}

Advanced Gin Features

package main

import (
    "fmt"
    "net/http"
    "time"
    
    "github.com/gin-gonic/gin"
    "github.com/golang-jwt/jwt/v4"
    "golang.org/x/crypto/bcrypt"
)

// JWT Claims structure
type Claims struct {
    UserID   int    `json:"user_id"`
    Username string `json:"username"`
    jwt.RegisteredClaims
}

// Login request structure
type LoginRequest struct {
    Username string `json:"username" binding:"required"`
    Password string `json:"password" binding:"required"`
}

// JWT secret key (in production, use environment variable)
var jwtSecret = []byte("your-secret-key")

// AuthService handles authentication
type AuthService struct {
    userService *UserService
}

func NewAuthService(userService *UserService) *AuthService {
    return &AuthService{userService: userService}
}

func (a *AuthService) GenerateToken(userID int, username string) (string, error) {
    claims := &Claims{
        UserID:   userID,
        Username: username,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            NotBefore: jwt.NewNumericDate(time.Now()),
        },
    }
    
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(jwtSecret)
}

func (a *AuthService) ValidateToken(tokenString string) (*Claims, error) {
    token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
        return jwtSecret, nil
    })
    
    if err != nil {
        return nil, err
    }
    
    if claims, ok := token.Claims.(*Claims); ok && token.Valid {
        return claims, nil
    }
    
    return nil, fmt.Errorf("invalid token")
}

// AuthController handles authentication endpoints
type AuthController struct {
    authService *AuthService
    userService *UserService
}

func NewAuthController(authService *AuthService, userService *UserService) *AuthController {
    return &AuthController{
        authService: authService,
        userService: userService,
    }
}

func (ac *AuthController) Login(c *gin.Context) {
    var req LoginRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, Response{
            Success: false,
            Error:   err.Error(),
        })
        return
    }
    
    // In a real application, you would verify password against hashed version
    // For this example, we'll just check if user exists
    users := ac.userService.GetAllUsers()
    var foundUser *User
    
    for _, user := range users {
        if user.Username == req.Username {
            foundUser = user
            break
        }
    }
    
    if foundUser == nil {
        c.JSON(http.StatusUnauthorized, Response{
            Success: false,
            Error:   "Invalid credentials",
        })
        return
    }
    
    // Generate JWT token
    token, err := ac.authService.GenerateToken(foundUser.ID, foundUser.Username)
    if err != nil {
        c.JSON(http.StatusInternalServerError, Response{
            Success: false,
            Error:   "Failed to generate token",
        })
        return
    }
    
    c.JSON(http.StatusOK, Response{
        Success: true,
        Data: gin.H{
            "token": token,
            "user":  foundUser,
        },
    })
}

// JWT Authentication middleware
func JWTAuthMiddleware(authService *AuthService) gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.JSON(http.StatusUnauthorized, Response{
                Success: false,
                Error:   "Authorization header required",
            })
            c.Abort()
            return
        }
        
        // Extract token from "Bearer <token>"
        tokenString := ""
        if len(authHeader) > 7 && authHeader[:7] == "Bearer " {
            tokenString = authHeader[7:]
        } else {
            c.JSON(http.StatusUnauthorized, Response{
                Success: false,
                Error:   "Invalid authorization header format",
            })
            c.Abort()
            return
        }
        
        claims, err := authService.ValidateToken(tokenString)
        if err != nil {
            c.JSON(http.StatusUnauthorized, Response{
                Success: false,
                Error:   "Invalid token",
            })
            c.Abort()
            return
        }
        
        // Add user info to context
        c.Set("userID", claims.UserID)
        c.Set("username", claims.Username)
        c.Next()
    }
}

// Rate limiting middleware
func RateLimitMiddleware() gin.HandlerFunc {
    // Simple in-memory rate limiter (use Redis in production)
    requests := make(map[string][]time.Time)
    
    return func(c *gin.Context) {
        clientIP := c.ClientIP()
        now := time.Now()
        
        // Clean old requests (older than 1 minute)
        if times, exists := requests[clientIP]; exists {
            var validTimes []time.Time
            for _, t := range times {
                if now.Sub(t) < time.Minute {
                    validTimes = append(validTimes, t)
                }
            }
            requests[clientIP] = validTimes
        }
        
        // Check rate limit (max 100 requests per minute)
        if len(requests[clientIP]) >= 100 {
            c.JSON(http.StatusTooManyRequests, Response{
                Success: false,
                Error:   "Rate limit exceeded",
            })
            c.Abort()
            return
        }
        
        // Add current request
        requests[clientIP] = append(requests[clientIP], now)
        c.Next()
    }
}

// File upload endpoint
func (c *UserController) UploadAvatar(ctx *gin.Context) {
    userID := ctx.GetInt("userID")
    
    file, err := ctx.FormFile("avatar")
    if err != nil {
        ctx.JSON(http.StatusBadRequest, Response{
            Success: false,
            Error:   "No file uploaded",
        })
        return
    }
    
    // Validate file type
    if !strings.HasPrefix(file.Header.Get("Content-Type"), "image/") {
        ctx.JSON(http.StatusBadRequest, Response{
            Success: false,
            Error:   "Only image files are allowed",
        })
        return
    }
    
    // Validate file size (max 5MB)
    if file.Size > 5*1024*1024 {
        ctx.JSON(http.StatusBadRequest, Response{
            Success: false,
            Error:   "File size too large (max 5MB)",
        })
        return
    }
    
    // Generate unique filename
    filename := fmt.Sprintf("avatar_%d_%d.jpg", userID, time.Now().Unix())
    filepath := fmt.Sprintf("uploads/%s", filename)
    
    // Save file
    if err := ctx.SaveUploadedFile(file, filepath); err != nil {
        ctx.JSON(http.StatusInternalServerError, Response{
            Success: false,
            Error:   "Failed to save file",
        })
        return
    }
    
    ctx.JSON(http.StatusOK, Response{
        Success: true,
        Data: gin.H{
            "filename": filename,
            "url":      fmt.Sprintf("/uploads/%s", filename),
        },
    })
}

func setupAdvancedRoutes() *gin.Engine {
    r := gin.Default()
    
    // Initialize services
    userService := NewUserService()
    authService := NewAuthService(userService)
    userController := NewUserController(userService)
    authController := NewAuthController(authService, userService)
    
    // Global middleware
    r.Use(LoggerMiddleware())
    r.Use(CORSMiddleware())
    r.Use(ErrorHandlerMiddleware())
    r.Use(RateLimitMiddleware())
    
    // Public routes
    public := r.Group("/api/v1")
    {
        public.POST("/auth/login", authController.Login)
        public.POST("/users", userController.CreateUser) // User registration
    }
    
    // Protected routes (require authentication)
    protected := r.Group("/api/v1")
    protected.Use(JWTAuthMiddleware(authService))
    {
        users := protected.Group("/users")
        {
            users.GET("", userController.GetAllUsers)
            users.GET("/:id", userController.GetUser)
            users.PUT("/:id", userController.UpdateUser)
            users.DELETE("/:id", userController.DeleteUser)
            users.POST("/avatar", userController.UploadAvatar)
        }
    }
    
    // Serve static files
    r.Static("/uploads", "./uploads")
    
    // Health check
    r.GET("/health", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "status":    "healthy",
            "timestamp": time.Now().UTC(),
            "version":   "1.0.0",
        })
    })
    
    return r
}

Understanding GORM

GORM is a developer-friendly ORM library for Go that makes database operations more intuitive while maintaining performance.

Basic GORM Usage

package main

import (
    "fmt"
    "log"
    "time"
    
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
)

// User model with GORM tags
type User struct {
    ID        uint           `json:"id" gorm:"primaryKey"`
    CreatedAt time.Time      `json:"created_at"`
    UpdatedAt time.Time      `json:"updated_at"`
    DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
    
    Username  string     `json:"username" gorm:"uniqueIndex;size:50;not null"`
    Email     string     `json:"email" gorm:"uniqueIndex;size:255;not null"`
    FullName  string     `json:"full_name" gorm:"size:255;not null"`
    Password  string     `json:"-" gorm:"size:255;not null"` // Hidden from JSON
    Age       int        `json:"age" gorm:"check:age >= 0 AND age <= 130"`
    IsActive  bool       `json:"is_active" gorm:"default:true"`
    
    // Relationships
    Posts    []Post    `json:"posts,omitempty" gorm:"foreignKey:AuthorID"`
    Profile  Profile   `json:"profile,omitempty" gorm:"foreignKey:UserID"`
    Comments []Comment `json:"comments,omitempty" gorm:"foreignKey:AuthorID"`
}

// Profile model (one-to-one with User)
type Profile struct {
    ID     uint   `json:"id" gorm:"primaryKey"`
    UserID uint   `json:"user_id" gorm:"uniqueIndex;not null"`
    Bio    string `json:"bio" gorm:"type:text"`
    Avatar string `json:"avatar" gorm:"size:500"`
    
    CreatedAt time.Time      `json:"created_at"`
    UpdatedAt time.Time      `json:"updated_at"`
    DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
    
    User User `json:"user,omitempty" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
}

// Post model
type Post struct {
    ID       uint   `json:"id" gorm:"primaryKey"`
    Title    string `json:"title" gorm:"size:255;not null"`
    Content  string `json:"content" gorm:"type:text;not null"`
    AuthorID uint   `json:"author_id" gorm:"not null"`
    Status   string `json:"status" gorm:"size:20;default:'draft';check:status IN ('draft', 'published', 'archived')"`
    
    PublishedAt *time.Time     `json:"published_at"`
    CreatedAt   time.Time      `json:"created_at"`
    UpdatedAt   time.Time      `json:"updated_at"`
    DeletedAt   gorm.DeletedAt `json:"-" gorm:"index"`
    
    // Relationships
    Author   User      `json:"author,omitempty" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
    Comments []Comment `json:"comments,omitempty" gorm:"foreignKey:PostID"`
    Tags     []Tag     `json:"tags,omitempty" gorm:"many2many:post_tags;"`
}

// Comment model
type Comment struct {
    ID       uint   `json:"id" gorm:"primaryKey"`
    Content  string `json:"content" gorm:"type:text;not null"`
    PostID   uint   `json:"post_id" gorm:"not null"`
    AuthorID uint   `json:"author_id" gorm:"not null"`
    
    CreatedAt time.Time      `json:"created_at"`
    UpdatedAt time.Time      `json:"updated_at"`
    DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
    
    // Relationships
    Post   Post `json:"post,omitempty" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
    Author User `json:"author,omitempty" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
}

// Tag model (many-to-many with Post)
type Tag struct {
    ID   uint   `json:"id" gorm:"primaryKey"`
    Name string `json:"name" gorm:"uniqueIndex;size:50;not null"`
    
    CreatedAt time.Time      `json:"created_at"`
    UpdatedAt time.Time      `json:"updated_at"`
    DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
    
    Posts []Post `json:"posts,omitempty" gorm:"many2many:post_tags;"`
}

// Database configuration
type DatabaseConfig struct {
    Host     string
    Port     int
    Database string
    Username string
    Password string
    SSLMode  string
}

func (cfg DatabaseConfig) DSN() string {
    return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
        cfg.Host, cfg.Port, cfg.Username, cfg.Password, cfg.Database, cfg.SSLMode)
}

// Database service
type DatabaseService struct {
    DB *gorm.DB
}

func NewDatabaseService(config DatabaseConfig) (*DatabaseService, error) {
    // Configure GORM logger
    newLogger := logger.New(
        log.New(os.Stdout, "\r\n", log.LstdFlags),
        logger.Config{
            SlowThreshold:             time.Second,
            LogLevel:                  logger.Info,
            IgnoreRecordNotFoundError: true,
            Colorful:                  true,
        },
    )
    
    db, err := gorm.Open(postgres.Open(config.DSN()), &gorm.Config{
        Logger: newLogger,
    })
    if err != nil {
        return nil, fmt.Errorf("failed to connect to database: %w", err)
    }
    
    // Configure connection pool
    sqlDB, err := db.DB()
    if err != nil {
        return nil, fmt.Errorf("failed to get database instance: %w", err)
    }
    
    sqlDB.SetMaxOpenConns(25)
    sqlDB.SetMaxIdleConns(5)
    sqlDB.SetConnMaxLifetime(5 * time.Minute)
    
    return &DatabaseService{DB: db}, nil
}

func (ds *DatabaseService) AutoMigrate() error {
    return ds.DB.AutoMigrate(&User{}, &Profile{}, &Post{}, &Comment{}, &Tag{})
}

// Repository pattern with GORM
type UserRepository struct {
    db *gorm.DB
}

func NewUserRepository(db *gorm.DB) *UserRepository {
    return &UserRepository{db: db}
}

func (r *UserRepository) Create(user *User) error {
    // Hash password before saving
    hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
    if err != nil {
        return fmt.Errorf("failed to hash password: %w", err)
    }
    user.Password = string(hashedPassword)
    
    return r.db.Create(user).Error
}

func (r *UserRepository) GetByID(id uint) (*User, error) {
    var user User
    err := r.db.Preload("Profile").Preload("Posts").First(&user, id).Error
    if err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            return nil, fmt.Errorf("user with id %d not found", id)
        }
        return nil, err
    }
    return &user, nil
}

func (r *UserRepository) GetByUsername(username string) (*User, error) {
    var user User
    err := r.db.Where("username = ?", username).First(&user).Error
    if err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            return nil, fmt.Errorf("user with username %s not found", username)
        }
        return nil, err
    }
    return &user, nil
}

func (r *UserRepository) GetAll(limit, offset int) ([]User, error) {
    var users []User
    err := r.db.Preload("Profile").Limit(limit).Offset(offset).Find(&users).Error
    return users, err
}

func (r *UserRepository) Update(user *User) error {
    return r.db.Save(user).Error
}

func (r *UserRepository) Delete(id uint) error {
    return r.db.Delete(&User{}, id).Error
}

func (r *UserRepository) Count() (int64, error) {
    var count int64
    err := r.db.Model(&User{}).Count(&count).Error
    return count, err
}

// Search users with filters
func (r *UserRepository) Search(query string, isActive *bool, limit, offset int) ([]User, error) {
    db := r.db.Model(&User{}).Preload("Profile")
    
    if query != "" {
        db = db.Where("username ILIKE ? OR full_name ILIKE ? OR email ILIKE ?", 
            "%"+query+"%", "%"+query+"%", "%"+query+"%")
    }
    
    if isActive != nil {
        db = db.Where("is_active = ?", *isActive)
    }
    
    var users []User
    err := db.Limit(limit).Offset(offset).Find(&users).Error
    return users, err
}

// Post repository
type PostRepository struct {
    db *gorm.DB
}

func NewPostRepository(db *gorm.DB) *PostRepository {
    return &PostRepository{db: db}
}

func (r *PostRepository) Create(post *Post) error {
    return r.db.Create(post).Error
}

func (r *PostRepository) GetByID(id uint) (*Post, error) {
    var post Post
    err := r.db.Preload("Author").Preload("Comments.Author").Preload("Tags").First(&post, id).Error
    if err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            return nil, fmt.Errorf("post with id %d not found", id)
        }
        return nil, err
    }
    return &post, nil
}

func (r *PostRepository) GetPublished(limit, offset int) ([]Post, error) {
    var posts []Post
    err := r.db.Where("status = ?", "published").
        Preload("Author").
        Preload("Tags").
        Order("published_at DESC").
        Limit(limit).
        Offset(offset).
        Find(&posts).Error
    return posts, err
}

func (r *PostRepository) GetByAuthor(authorID uint, limit, offset int) ([]Post, error) {
    var posts []Post
    err := r.db.Where("author_id = ?", authorID).
        Preload("Tags").
        Order("created_at DESC").
        Limit(limit).
        Offset(offset).
        Find(&posts).Error
    return posts, err
}

func (r *PostRepository) Search(query string, tags []string, status string, limit, offset int) ([]Post, error) {
    db := r.db.Model(&Post{}).Preload("Author").Preload("Tags")
    
    if query != "" {
        db = db.Where("title ILIKE ? OR content ILIKE ?", "%"+query+"%", "%"+query+"%")
    }
    
    if status != "" {
        db = db.Where("status = ?", status)
    }
    
    if len(tags) > 0 {
        db = db.Joins("JOIN post_tags ON posts.id = post_tags.post_id").
            Joins("JOIN tags ON post_tags.tag_id = tags.id").
            Where("tags.name IN ?", tags).
            Group("posts.id")
    }
    
    var posts []Post
    err := db.Limit(limit).Offset(offset).Find(&posts).Error
    return posts, err
}

func (r *PostRepository) Update(post *Post) error {
    return r.db.Save(post).Error
}

func (r *PostRepository) Delete(id uint) error {
    return r.db.Delete(&Post{}, id).Error
}

// Advanced GORM operations
func (r *PostRepository) GetPopularPosts(limit int, since time.Time) ([]Post, error) {
    var posts []Post
    
    // Raw SQL for complex queries
    err := r.db.Raw(`
        SELECT p.*, COUNT(c.id) as comment_count
        FROM posts p
        LEFT JOIN comments c ON p.id = c.post_id
        WHERE p.status = 'published' AND p.published_at >= ?
        GROUP BY p.id
        ORDER BY comment_count DESC, p.published_at DESC
        LIMIT ?
    `, since, limit).Scan(&posts).Error
    
    if err != nil {
        return nil, err
    }
    
    // Load associations
    for i := range posts {
        r.db.Preload("Author").Preload("Tags").First(&posts[i], posts[i].ID)
    }
    
    return posts, nil
}

// Transaction example
func (r *PostRepository) PublishPost(postID uint) error {
    return r.db.Transaction(func(tx *gorm.DB) error {
        // Update post status
        if err := tx.Model(&Post{}).Where("id = ?", postID).Updates(map[string]interface{}{
            "status":       "published",
            "published_at": time.Now(),
        }).Error; err != nil {
            return err
        }
        
        // Create notification (example additional operation)
        // In a real app, you might notify followers, update search index, etc.
        
        return nil
    })
}

// Batch operations
func (r *PostRepository) BulkUpdateStatus(postIDs []uint, status string) error {
    return r.db.Model(&Post{}).Where("id IN ?", postIDs).Update("status", status).Error
}

func main() {
    // Database configuration
    config := DatabaseConfig{
        Host:     "localhost",
        Port:     5432,
        Database: "blog_db",
        Username: "user",
        Password: "password",
        SSLMode:  "disable",
    }
    
    // Initialize database
    dbService, err := NewDatabaseService(config)
    if err != nil {
        log.Fatal("Failed to connect to database:", err)
    }
    
    // Auto-migrate models
    if err := dbService.AutoMigrate(); err != nil {
        log.Fatal("Failed to migrate database:", err)
    }
    
    // Initialize repositories
    userRepo := NewUserRepository(dbService.DB)
    postRepo := NewPostRepository(dbService.DB)
    
    // Example usage
    fmt.Println("Database connected and migrated successfully!")
    
    // Create sample user
    user := &User{
        Username: "johndoe",
        Email:    "john@example.com",
        FullName: "John Doe",
        Password: "password123",
        Age:      30,
    }
    
    if err := userRepo.Create(user); err != nil {
        log.Printf("Failed to create user: %v", err)
    } else {
        log.Printf("Created user with ID: %d", user.ID)
    }
}

5. Live Coding Demonstration (20-25 minutes)

Example 1: Complete Blog API with Echo Framework

package main

import (
    "net/http"
    "strconv"
    
    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
    "github.com/go-playground/validator/v10"
)

// Custom validator
type CustomValidator struct {
    validator *validator.Validate
}

func (cv *CustomValidator) Validate(i interface{}) error {
    return cv.validator.Struct(i)
}

// Echo application setup
func NewEchoApp() *echo.Echo {
    e := echo.New()
    
    // Set custom validator
    e.Validator = &CustomValidator{validator: validator.New()}
    
    // Middleware
    e.Use(middleware.Logger())
    e.Use(middleware.Recover())
    e.Use(middleware.CORS())
    e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(100)))
    
    return e
}

// Echo handlers
func (c *UserController) EchoCreateUser(ctx echo.Context) error {
    var user User
    
    if err := ctx.Bind(&user); err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, "Invalid input")
    }
    
    if err := ctx.Validate(&user); err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, err.Error())
    }
    
    if err := c.service.CreateUser(&user); err != nil {
        if strings.Contains(err.Error(), "already exists") {
            return echo.NewHTTPError(http.StatusConflict, err.Error())
        }
        return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user")
    }
    
    return ctx.JSON(http.StatusCreated, Response{
        Success: true,
        Message: "User created successfully",
        Data:    user,
    })
}

func (c *UserController) EchoGetUser(ctx echo.Context) error {
    id, err := strconv.Atoi(ctx.Param("id"))
    if err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, "Invalid user ID")
    }
    
    user, err := c.service.GetUser(id)
    if err != nil {
        return echo.NewHTTPError(http.StatusNotFound, err.Error())
    }
    
    return ctx.JSON(http.StatusOK, Response{
        Success: true,
        Data:    user,
    })
}

// Setup Echo routes
func setupEchoRoutes(e *echo.Echo, userController *UserController, authController *AuthController) {
    // API group
    api := e.Group("/api/v1")
    
    // Public routes
    api.POST("/auth/login", authController.EchoLogin)
    api.POST("/users", userController.EchoCreateUser)
    
    // Protected routes
    protected := api.Group("/users")
    protected.Use(middleware.JWT([]byte("secret")))
    {
        protected.GET("", userController.EchoGetAllUsers)
        protected.GET("/:id", userController.EchoGetUser)
        protected.PUT("/:id", userController.EchoUpdateUser)
        protected.DELETE("/:id", userController.EchoDeleteUser)
    }
    
    // Health check
    e.GET("/health", func(c echo.Context) error {
        return c.JSON(http.StatusOK, map[string]interface{}{
            "status": "healthy",
            "time":   time.Now().UTC(),
        })
    })
}

func runEchoServer() {
    e := NewEchoApp()
    
    // Initialize services
    userService := NewUserService()
    authService := NewAuthService(userService)
    userController := NewUserController(userService)
    authController := NewAuthController(authService, userService)
    
    // Setup routes
    setupEchoRoutes(e, userController, authController)
    
    // Start server
    e.Logger.Fatal(e.Start(":8081"))
}

Example 2: Production-Ready Application Structure

project/
├── cmd/
│   └── server/
│       └── main.go
├── internal/
│   ├── api/
│   │   ├── handlers/
│   │   ├── middleware/
│   │   └── routes/
│   ├── config/
│   ├── domain/
│   │   ├── models/
│   │   └── services/
│   ├── infrastructure/
│   │   ├── database/
│   │   ├── logger/
│   │   └── auth/
│   └── repository/
├── pkg/
│   ├── errors/
│   ├── utils/
│   └── validator/
├── migrations/
├── docker/
├── scripts/
├── .env
├── docker-compose.yml
├── Dockerfile
├── go.mod
└── go.sum
// cmd/server/main.go
package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "time"
    
    "github.com/yourproject/internal/api"
    "github.com/yourproject/internal/config"
    "github.com/yourproject/internal/infrastructure/database"
    "github.com/yourproject/internal/infrastructure/logger"
)

func main() {
    // Load configuration
    cfg, err := config.Load()
    if err != nil {
        log.Fatal("Failed to load configuration:", err)
    }
    
    // Initialize logger
    logger := logger.New(cfg.Logger)
    
    // Initialize database
    db, err := database.New(cfg.Database)
    if err != nil {
        logger.Fatal("Failed to connect to database", "error", err)
    }
    defer db.Close()
    
    // Initialize API server
    server := api.NewServer(cfg, db, logger)
    
    // Start server
    go func() {
        if err := server.Start(); err != nil && err != http.ErrServerClosed {
            logger.Fatal("Failed to start server", "error", err)
        }
    }()
    
    logger.Info("Server started", "port", cfg.Server.Port)
    
    // Wait for interrupt signal to gracefully shutdown
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, os.Interrupt)
    <-quit
    
    logger.Info("Shutting down server...")
    
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    
    if err := server.Shutdown(ctx); err != nil {
        logger.Fatal("Server forced to shutdown", "error", err)
    }
    
    logger.Info("Server exited")
}
// internal/config/config.go
package config

import (
    "github.com/spf13/viper"
)

type Config struct {
    Server   ServerConfig   `mapstructure:"server"`
    Database DatabaseConfig `mapstructure:"database"`
    Logger   LoggerConfig   `mapstructure:"logger"`
    JWT      JWTConfig      `mapstructure:"jwt"`
    Redis    RedisConfig    `mapstructure:"redis"`
}

type ServerConfig struct {
    Port         string        `mapstructure:"port"`
    ReadTimeout  time.Duration `mapstructure:"read_timeout"`
    WriteTimeout time.Duration `mapstructure:"write_timeout"`
}

type DatabaseConfig struct {
    Host     string `mapstructure:"host"`
    Port     int    `mapstructure:"port"`
    Database string `mapstructure:"database"`
    Username string `mapstructure:"username"`
    Password string `mapstructure:"password"`
    SSLMode  string `mapstructure:"ssl_mode"`
}

type LoggerConfig struct {
    Level  string `mapstructure:"level"`
    Format string `mapstructure:"format"`
}

type JWTConfig struct {
    Secret     string        `mapstructure:"secret"`
    Expiration time.Duration `mapstructure:"expiration"`
}

type RedisConfig struct {
    Host     string `mapstructure:"host"`
    Port     int    `mapstructure:"port"`
    Password string `mapstructure:"password"`
    DB       int    `mapstructure:"db"`
}

func Load() (*Config, error) {
    viper.SetConfigName("config")
    viper.SetConfigType("yaml")
    viper.AddConfigPath(".")
    viper.AddConfigPath("./config")
    
    // Environment variables
    viper.AutomaticEnv()
    viper.SetEnvPrefix("APP")
    
    // Default values
    viper.SetDefault("server.port", "8080")
    viper.SetDefault("server.read_timeout", "15s")
    viper.SetDefault("server.write_timeout", "15s")
    viper.SetDefault("logger.level", "info")
    viper.SetDefault("logger.format", "json")
    viper.SetDefault("jwt.expiration", "24h")
    
    if err := viper.ReadInConfig(); err != nil {
        if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
            return nil, err
        }
    }
    
    var config Config
    if err := viper.Unmarshal(&config); err != nil {
        return nil, err
    }
    
    return &config, nil
}
# Dockerfile
FROM golang:1.21-alpine AS builder

WORKDIR /app

# Install dependencies
RUN apk add --no-cache git

# Copy go mod files
COPY go.mod go.sum ./
RUN go mod download

# Copy source code
COPY . .

# Build the application
RUN CGO_ENABLED=0 GOOS=linux go build -o main cmd/server/main.go

# Final stage
FROM alpine:latest

RUN apk --no-cache add ca-certificates tzdata
WORKDIR /root/

# Copy the binary
COPY --from=builder /app/main .
COPY --from=builder /app/config ./config

# Expose port
EXPOSE 8080

# Run the application
CMD ["./main"]
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - APP_DATABASE_HOST=postgres
      - APP_DATABASE_PORT=5432
      - APP_DATABASE_DATABASE=appdb
      - APP_DATABASE_USERNAME=user
      - APP_DATABASE_PASSWORD=password
      - APP_REDIS_HOST=redis
      - APP_REDIS_PORT=6379
    depends_on:
      - postgres
      - redis
    volumes:
      - ./config:/root/config

  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: appdb
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data

volumes:
  postgres_data:
  redis_data:

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

Exercise 1: Build a Task Management API (15 minutes)

Objective: Create a complete task management system with Gin and GORM

Instructions:

  1. Design models for tasks, projects, and users
  2. Implement CRUD operations with proper validation
  3. Add authentication and authorization
  4. Include search and filtering capabilities

Starter Code:

type Task struct {
    ID          uint      `json:"id" gorm:"primaryKey"`
    Title       string    `json:"title" binding:"required,min=1,max=200"`
    Description string    `json:"description"`
    Status      string    `json:"status" binding:"required,oneof=todo in_progress done"`
    Priority    string    `json:"priority" binding:"required,oneof=low medium high"`
    DueDate     time.Time `json:"due_date"`
    ProjectID   uint      `json:"project_id"`
    AssigneeID  uint      `json:"assignee_id"`
}

func main() {
    // TODO: Implement complete task management API
}

Exercise 2: Add Real-time Features (10 minutes)

Objective: Implement WebSocket notifications for task updates

Requirements:

  • Real-time notifications when tasks are created/updated
  • User-specific notification channels
  • Connection management and cleanup
  • Integration with existing REST API

Hints:

  • Use gorilla/websocket for WebSocket support
  • Consider using Redis for pub/sub messaging
  • Implement proper error handling and reconnection logic

Exercise 3: Performance Optimization (15 minutes)

Objective: Optimize the application for production use

Challenge: Add:

  • Response caching with Redis
  • Database query optimization
  • Rate limiting and request throttling
  • Monitoring and metrics collection

Extension Ideas:

  • Implement circuit breaker pattern
  • Add distributed tracing
  • Set up health checks and readiness probes

7. Common Pitfalls and Troubleshooting (10 minutes)

Frequent Mistakes

  1. Problem: N+1 query problems with GORM Cause: Not using Preload for related data Solution: Use appropriate preloading strategies
// Wrong - causes N+1 queries
users, _ := db.Find(&users)
for _, user := range users {
    db.Model(&user).Association("Posts").Find(&user.Posts)
}

// Right - preloads in single query
db.Preload("Posts").Find(&users)
  1. Problem: Memory leaks with Gin handlers Cause: Not properly handling goroutines and context Solution: Use proper context management and cleanup

Debugging Tips

  • Use GORM’s debug mode to see SQL queries
  • Implement proper logging and request tracing
  • Use profiling tools like pprof for performance analysis
  • Monitor database connection pools

8. Best Practices and Professional Tips (5 minutes)

Industry Standards

  • Project Structure: Follow standard Go project layout
  • Error Handling: Use custom error types and proper HTTP status codes
  • Configuration: Use environment variables and config files
  • Testing: Write comprehensive tests for all layers

Security Considerations

// Input validation
func validateUser(user *User) error {
    if err := validate.Struct(user); err != nil {
        return err
    }
    // Additional business logic validation
    return nil
}

// SQL injection prevention (GORM handles this automatically)
db.Where("username = ?", username).First(&user)

// Password hashing
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)

Performance Tips

  • Use connection pooling for databases
  • Implement caching for frequently accessed data
  • Use appropriate database indexes
  • Consider using prepared statements for repeated queries

Next Steps in Your Learning Journey

  • Explore microservices architecture with Go
  • Learn about containerization and Kubernetes deployment
  • Study advanced patterns like CQRS and Event Sourcing
  • Practice with distributed systems and message queues

9. Summary and Review (5 minutes)

Key Takeaways

  1. Go frameworks provide excellent performance while maintaining simplicity
  2. GORM makes database operations more intuitive without sacrificing control
  3. Proper project structure is crucial for maintainable applications
  4. Testing and monitoring are essential for production readiness

Quick Review Questions

  1. What are the main differences between Gin and Echo frameworks?
  2. How does GORM handle relationships between models?
  3. What are the essential middleware components for a production API?

For Instructors

Teaching Tips

  • Demonstrate both simple examples and production-ready patterns
  • Show how to debug common issues with frameworks
  • Emphasize the importance of proper error handling and validation
  • Use real-world examples that students can relate to

Assessment Ideas

  • Build a complete REST API with authentication and database integration
  • Implement a microservice with proper monitoring and health checks
  • Create a deployment pipeline with Docker and CI/CD

Adaptation Notes

  • For beginners: Focus on basic framework usage and simple CRUD operations
  • For advanced students: Include advanced patterns like middleware chains and custom validators
  • Online delivery: Provide Docker containers with pre-configured services for practice