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:
- Gin Framework - High-performance HTTP web framework
- Echo Framework - Minimalist, fast web framework
- GORM - Developer-friendly ORM for database operations
- Popular Libraries - Logging, validation, configuration, and more
- Authentication & Authorization - JWT, middleware, and security
- Project Structure - Best practices for organizing Go applications
- 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.
Popular Framework Categories
- 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:
- Design models for tasks, projects, and users
- Implement CRUD operations with proper validation
- Add authentication and authorization
- 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
- 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)
- 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
- Go frameworks provide excellent performance while maintaining simplicity
- GORM makes database operations more intuitive without sacrificing control
- Proper project structure is crucial for maintainable applications
- Testing and monitoring are essential for production readiness
Quick Review Questions
- What are the main differences between Gin and Echo frameworks?
- How does GORM handle relationships between models?
- 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