Redis with Golang - Basic Integration

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

  • Set up and configure the go-redis client library in your Go applications
  • Establish Redis connections with proper error handling and connection pooling
  • Implement all Redis data type operations using Go code
  • Handle Redis errors gracefully and implement retry logic
  • Structure Go applications with clean Redis integration patterns

1. Welcome and Introduction (5 minutes)

Icebreaker Activity

Have you ever wondered how to bridge the gap between Redis’s powerful CLI commands and your Go applications? How do popular Go services like Docker, Kubernetes, and Grafana integrate with Redis for caching and data storage? Today we’ll learn how to make Redis and Go work together seamlessly!

Lesson Overview

  • Duration: 90 minutes
  • Prerequisites: Go fundamentals, Redis basics (Lessons 1-2), understanding of Go modules and packages
  • Materials Needed: Go 1.19+, Redis server, code editor with Go support

2. Learning Objectives and Session Outline (5 minutes)

What We’ll Cover Today:

  1. Go-Redis Library Setup - Installing and configuring the go-redis client
  2. Connection Management - Connection pooling, timeouts, and error handling
  3. Basic Operations - Implementing string, hash, list, set, and sorted set operations
  4. Error Handling - Graceful error handling and retry mechanisms
  5. Best Practices - Code organization, testing, and performance considerations
  6. Real-World Integration - Building a complete Redis service layer

Success Criteria:

By the end of this session, you should be able to create production-ready Go applications that effectively use Redis for caching, data storage, and real-time features.

3. Foundation Knowledge (15-20 minutes)

Core Concepts

Why Go-Redis?

Go-redis is the most popular Redis client library for Go, providing a clean, idiomatic Go interface to Redis operations. It offers excellent performance, comprehensive feature support, and active maintenance.

Real-World Analogy: Think of go-redis as a translator between your Go application and Redis. Just like how a skilled translator helps you communicate naturally in a foreign language, go-redis lets you work with Redis using familiar Go patterns and idioms.

Why This Matters: Proper Redis integration can make your Go applications 10x faster by eliminating database bottlenecks, enabling real-time features, and providing horizontal scaling capabilities.

Key Features of Go-Redis

  1. Type Safety: Compile-time checking for Redis operations
  2. Connection Pooling: Automatic connection management for high concurrency
  3. Pipeline Support: Batch operations for better performance
  4. Pub/Sub: Real-time messaging capabilities
  5. Cluster Support: Distributed Redis deployments
  6. Context Support: Proper cancellation and timeout handling

Key Terminology

  • Client: The Redis connection object in go-redis
  • Context: Go’s context package for cancellation and timeouts
  • Pipeline: Batching multiple Redis commands for efficiency
  • Connection Pool: Reusable Redis connections managed by the client
  • Marshaling: Converting Go data structures to Redis-compatible formats
  • Sentinel: Redis high availability solution

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

Setting Up Go-Redis

Installing Dependencies

# Initialize Go module (if not already done)
go mod init redis-tutorial

# Add go-redis dependency
go get github.com/redis/go-redis/v9

# Optional: Add JSON support
go get encoding/json

Basic Client Setup

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "github.com/redis/go-redis/v9"
)

func main() {
    // Create Redis client
    rdb := redis.NewClient(&redis.Options{
        Addr:     "localhost:6379",
        Password: "", // no password
        DB:       0,  // default DB
    })
    defer rdb.Close()

    // Test connection
    ctx := context.Background()
    pong, err := rdb.Ping(ctx).Result()
    if err != nil {
        log.Fatal("Failed to connect to Redis:", err)
    }
    fmt.Println("Connected to Redis:", pong)
}

Advanced Client Configuration

func createRedisClient() *redis.Client {
    return redis.NewClient(&redis.Options{
        Addr:         "localhost:6379",
        Password:     "",
        DB:           0,
        PoolSize:     10,              // Connection pool size
        MinIdleConns: 5,               // Minimum idle connections
        MaxRetries:   3,               // Retry attempts
        DialTimeout:  5 * time.Second, // Connection timeout
        ReadTimeout:  3 * time.Second, // Read operation timeout
        WriteTimeout: 3 * time.Second, // Write operation timeout
        IdleTimeout:  5 * time.Minute, // Idle connection timeout
    })
}

Working with Redis Data Types in Go

String Operations

type StringOperations struct {
    client *redis.Client
    ctx    context.Context
}

func NewStringOperations(client *redis.Client) *StringOperations {
    return &StringOperations{
        client: client,
        ctx:    context.Background(),
    }
}

func (s *StringOperations) SetValue(key, value string, expiration time.Duration) error {
    return s.client.Set(s.ctx, key, value, expiration).Err()
}

func (s *StringOperations) GetValue(key string) (string, error) {
    val, err := s.client.Get(s.ctx, key).Result()
    if err == redis.Nil {
        return "", fmt.Errorf("key %s does not exist", key)
    }
    return val, err
}

func (s *StringOperations) IncrementCounter(key string) (int64, error) {
    return s.client.Incr(s.ctx, key).Result()
}

func (s *StringOperations) SetMultiple(data map[string]interface{}) error {
    return s.client.MSet(s.ctx, data).Err()
}

Hash Operations

type User struct {
    ID       int    `json:"id"`
    Name     string `json:"name"`
    Email    string `json:"email"`
    Age      int    `json:"age"`
}

type HashOperations struct {
    client *redis.Client
    ctx    context.Context
}

func NewHashOperations(client *redis.Client) *HashOperations {
    return &HashOperations{
        client: client,
        ctx:    context.Background(),
    }
}

func (h *HashOperations) SaveUser(userID string, user User) error {
    userMap := map[string]interface{}{
        "id":    user.ID,
        "name":  user.Name,
        "email": user.Email,
        "age":   user.Age,
    }
    return h.client.HSet(h.ctx, "user:"+userID, userMap).Err()
}

func (h *HashOperations) GetUser(userID string) (*User, error) {
    result, err := h.client.HGetAll(h.ctx, "user:"+userID).Result()
    if err != nil {
        return nil, err
    }
    
    if len(result) == 0 {
        return nil, fmt.Errorf("user %s not found", userID)
    }

    user := &User{}
    if id, ok := result["id"]; ok {
        fmt.Sscanf(id, "%d", &user.ID)
    }
    user.Name = result["name"]
    user.Email = result["email"]
    if age, ok := result["age"]; ok {
        fmt.Sscanf(age, "%d", &user.Age)
    }

    return user, nil
}

func (h *HashOperations) UpdateUserField(userID, field string, value interface{}) error {
    return h.client.HSet(h.ctx, "user:"+userID, field, value).Err()
}

List Operations

type ListOperations struct {
    client *redis.Client
    ctx    context.Context
}

func NewListOperations(client *redis.Client) *ListOperations {
    return &ListOperations{
        client: client,
        ctx:    context.Background(),
    }
}

func (l *ListOperations) AddToQueue(queueName, item string) error {
    return l.client.LPush(l.ctx, queueName, item).Err()
}

func (l *ListOperations) ProcessFromQueue(queueName string) (string, error) {
    result, err := l.client.RPop(l.ctx, queueName).Result()
    if err == redis.Nil {
        return "", fmt.Errorf("queue %s is empty", queueName)
    }
    return result, err
}

func (l *ListOperations) GetQueueItems(queueName string, start, stop int64) ([]string, error) {
    return l.client.LRange(l.ctx, queueName, start, stop).Result()
}

func (l *ListOperations) GetQueueLength(queueName string) (int64, error) {
    return l.client.LLen(l.ctx, queueName).Result()
}

Set Operations

type SetOperations struct {
    client *redis.Client
    ctx    context.Context
}

func NewSetOperations(client *redis.Client) *SetOperations {
    return &SetOperations{
        client: client,
        ctx:    context.Background(),
    }
}

func (s *SetOperations) AddToSet(setName string, members ...interface{}) error {
    return s.client.SAdd(s.ctx, setName, members...).Err()
}

func (s *SetOperations) GetSetMembers(setName string) ([]string, error) {
    return s.client.SMembers(s.ctx, setName).Result()
}

func (s *SetOperations) IsSetMember(setName string, member interface{}) (bool, error) {
    return s.client.SIsMember(s.ctx, setName, member).Result()
}

func (s *SetOperations) GetSetIntersection(sets ...string) ([]string, error) {
    return s.client.SInter(s.ctx, sets...).Result()
}

func (s *SetOperations) RemoveFromSet(setName string, members ...interface{}) error {
    return s.client.SRem(s.ctx, setName, members...).Err()
}

Common Misconceptions

  1. Misconception: “I need to create a new client for each operation” Reality: Use a single client instance with connection pooling for better performance

  2. Misconception: “Context is optional in go-redis” Reality: Always use context for timeout control and cancellation

  3. Misconception: “Error handling is the same as other Go libraries” Reality: Redis has specific error types like redis.Nil that need special handling

5. Live Coding Demonstration (20-25 minutes)

Example 1: Complete Redis Service Layer

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "time"

    "github.com/redis/go-redis/v9"
)

// RedisService wraps Redis operations with proper error handling
type RedisService struct {
    client *redis.Client
    ctx    context.Context
}

// NewRedisService creates a new Redis service with connection validation
func NewRedisService(addr, password string, db int) (*RedisService, error) {
    rdb := redis.NewClient(&redis.Options{
        Addr:         addr,
        Password:     password,
        DB:           db,
        PoolSize:     10,
        MinIdleConns: 5,
        MaxRetries:   3,
        DialTimeout:  5 * time.Second,
        ReadTimeout:  3 * time.Second,
        WriteTimeout: 3 * time.Second,
    })

    ctx := context.Background()
    
    // Test connection
    if err := rdb.Ping(ctx).Err(); err != nil {
        return nil, fmt.Errorf("failed to connect to Redis: %w", err)
    }

    return &RedisService{
        client: rdb,
        ctx:    ctx,
    }, nil
}

// Close closes the Redis connection
func (r *RedisService) Close() error {
    return r.client.Close()
}

// SetJSON stores a Go struct as JSON in Redis
func (r *RedisService) SetJSON(key string, value interface{}, expiration time.Duration) error {
    jsonData, err := json.Marshal(value)
    if err != nil {
        return fmt.Errorf("failed to marshal JSON: %w", err)
    }
    
    return r.client.Set(r.ctx, key, jsonData, expiration).Err()
}

// GetJSON retrieves and unmarshals JSON data from Redis
func (r *RedisService) GetJSON(key string, dest interface{}) error {
    val, err := r.client.Get(r.ctx, key).Result()
    if err == redis.Nil {
        return fmt.Errorf("key %s not found", key)
    } else if err != nil {
        return fmt.Errorf("failed to get key %s: %w", key, err)
    }

    if err := json.Unmarshal([]byte(val), dest); err != nil {
        return fmt.Errorf("failed to unmarshal JSON: %w", err)
    }

    return nil
}

// IncrementCounter atomically increments a counter with optional expiration
func (r *RedisService) IncrementCounter(key string, expiration time.Duration) (int64, error) {
    pipe := r.client.Pipeline()
    incrCmd := pipe.Incr(r.ctx, key)
    if expiration > 0 {
        pipe.Expire(r.ctx, key, expiration)
    }
    
    _, err := pipe.Exec(r.ctx)
    if err != nil {
        return 0, err
    }
    
    return incrCmd.Val(), nil
}

// Example usage structures
type Product struct {
    ID          int     `json:"id"`
    Name        string  `json:"name"`
    Price       float64 `json:"price"`
    Category    string  `json:"category"`
    InStock     bool    `json:"in_stock"`
    LastUpdated string  `json:"last_updated"`
}

func main() {
    // Initialize Redis service
    redis, err := NewRedisService("localhost:6379", "", 0)
    if err != nil {
        log.Fatal("Failed to create Redis service:", err)
    }
    defer redis.Close()

    // Example: Product caching
    product := Product{
        ID:          1,
        Name:        "Gaming Laptop",
        Price:       1299.99,
        Category:    "Electronics",
        InStock:     true,
        LastUpdated: time.Now().Format(time.RFC3339),
    }

    // Cache product for 1 hour
    err = redis.SetJSON("product:1", product, time.Hour)
    if err != nil {
        log.Printf("Failed to cache product: %v", err)
        return
    }

    // Retrieve cached product
    var cachedProduct Product
    err = redis.GetJSON("product:1", &cachedProduct)
    if err != nil {
        log.Printf("Failed to get cached product: %v", err)
        return
    }

    fmt.Printf("Cached product: %+v\n", cachedProduct)

    // Example: Page view counter
    views, err := redis.IncrementCounter("pageviews:product:1", 24*time.Hour)
    if err != nil {
        log.Printf("Failed to increment counter: %v", err)
        return
    }

    fmt.Printf("Product page views: %d\n", views)
}

Explanation:

  • Lines 15-35: RedisService wrapper with proper configuration and connection validation
  • Lines 50-65: SetJSON method for storing Go structs as JSON with error handling
  • Lines 67-80: GetJSON method with proper nil handling and unmarshaling
  • Lines 82-95: IncrementCounter using pipeline for atomic operations with expiration
  • Lines 120-150: Demonstration of caching and counter operations

Example 2: Advanced Error Handling and Retry Logic

package main

import (
    "context"
    "errors"
    "fmt"
    "log"
    "time"

    "github.com/redis/go-redis/v9"
)

// RedisClient with advanced error handling
type RedisClient struct {
    client     *redis.Client
    maxRetries int
    retryDelay time.Duration
}

func NewRedisClient(options *redis.Options) *RedisClient {
    return &RedisClient{
        client:     redis.NewClient(options),
        maxRetries: 3,
        retryDelay: 100 * time.Millisecond,
    }
}

// RetryableOperation performs Redis operations with retry logic
func (r *RedisClient) RetryableOperation(operation func() error) error {
    var lastErr error
    
    for attempt := 0; attempt <= r.maxRetries; attempt++ {
        if attempt > 0 {
            time.Sleep(r.retryDelay * time.Duration(attempt))
        }
        
        lastErr = operation()
        
        // Don't retry on certain errors
        if lastErr == nil {
            return nil
        }
        
        if errors.Is(lastErr, redis.Nil) {
            return lastErr // Key not found, don't retry
        }
        
        // Log retry attempt
        if attempt < r.maxRetries {
            log.Printf("Operation failed (attempt %d/%d): %v", 
                attempt+1, r.maxRetries+1, lastErr)
        }
    }
    
    return fmt.Errorf("operation failed after %d retries: %w", 
        r.maxRetries+1, lastErr)
}

// SafeGet performs a GET operation with retry logic
func (r *RedisClient) SafeGet(ctx context.Context, key string) (string, error) {
    var result string
    var err error
    
    operation := func() error {
        result, err = r.client.Get(ctx, key).Result()
        return err
    }
    
    if err := r.RetryableOperation(operation); err != nil {
        return "", err
    }
    
    return result, nil
}

// SafeSet performs a SET operation with retry logic
func (r *RedisClient) SafeSet(ctx context.Context, key string, value interface{}, expiration time.Duration) error {
    operation := func() error {
        return r.client.Set(ctx, key, value, expiration).Err()
    }
    
    return r.RetryableOperation(operation)
}

func main() {
    client := NewRedisClient(&redis.Options{
        Addr: "localhost:6379",
    })
    defer client.client.Close()

    ctx := context.Background()

    // Example with retry logic
    err := client.SafeSet(ctx, "test:retry", "hello", time.Minute)
    if err != nil {
        log.Printf("Failed to set value: %v", err)
    }

    value, err := client.SafeGet(ctx, "test:retry")
    if err != nil {
        log.Printf("Failed to get value: %v", err)
    } else {
        fmt.Printf("Retrieved value: %s\n", value)
    }
}

Example 3: Building a Caching Layer

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "time"

    "github.com/redis/go-redis/v9"
)

// CacheManager provides a high-level caching interface
type CacheManager struct {
    client *redis.Client
    prefix string
}

func NewCacheManager(client *redis.Client, prefix string) *CacheManager {
    return &CacheManager{
        client: client,
        prefix: prefix,
    }
}

func (c *CacheManager) buildKey(key string) string {
    return fmt.Sprintf("%s:%s", c.prefix, key)
}

// Get retrieves data from cache with JSON unmarshaling
func (c *CacheManager) Get(ctx context.Context, key string, dest interface{}) error {
    fullKey := c.buildKey(key)
    
    val, err := c.client.Get(ctx, fullKey).Result()
    if err == redis.Nil {
        return fmt.Errorf("cache miss for key: %s", key)
    } else if err != nil {
        return fmt.Errorf("cache error: %w", err)
    }

    return json.Unmarshal([]byte(val), dest)
}

// Set stores data in cache with JSON marshaling
func (c *CacheManager) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error {
    fullKey := c.buildKey(key)
    
    data, err := json.Marshal(value)
    if err != nil {
        return fmt.Errorf("marshal error: %w", err)
    }

    return c.client.Set(ctx, fullKey, data, expiration).Err()
}

// GetOrSet implements cache-aside pattern
func (c *CacheManager) GetOrSet(ctx context.Context, key string, dest interface{}, 
    expiration time.Duration, loadFunc func() (interface{}, error)) error {
    
    // Try to get from cache first
    if err := c.Get(ctx, key, dest); err == nil {
        return nil // Cache hit
    }

    // Cache miss, load data
    data, err := loadFunc()
    if err != nil {
        return fmt.Errorf("load function error: %w", err)
    }

    // Store in cache
    if err := c.Set(ctx, key, data, expiration); err != nil {
        log.Printf("Failed to cache data: %v", err)
        // Continue anyway, we have the data
    }

    // Copy loaded data to destination
    jsonData, _ := json.Marshal(data)
    return json.Unmarshal(jsonData, dest)
}

// Delete removes data from cache
func (c *CacheManager) Delete(ctx context.Context, key string) error {
    fullKey := c.buildKey(key)
    return c.client.Del(ctx, fullKey).Err()
}

// Example usage
type UserService struct {
    cache *CacheManager
}

func NewUserService(cache *CacheManager) *UserService {
    return &UserService{cache: cache}
}

func (u *UserService) GetUser(ctx context.Context, userID string) (*User, error) {
    var user User
    
    err := u.cache.GetOrSet(ctx, fmt.Sprintf("user:%s", userID), &user,
        30*time.Minute, func() (interface{}, error) {
            // Simulate database query
            log.Printf("Loading user %s from database", userID)
            return &User{
                ID:    1,
                Name:  "John Doe",
                Email: "john@example.com",
                Age:   30,
            }, nil
        })

    if err != nil {
        return nil, err
    }

    return &user, nil
}

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
    Age   int    `json:"age"`
}

func main() {
    client := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })
    defer client.Close()

    cache := NewCacheManager(client, "myapp")
    userService := NewUserService(cache)

    ctx := context.Background()

    // First call - cache miss, loads from "database"
    user1, err := userService.GetUser(ctx, "123")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("First call: %+v\n", user1)

    // Second call - cache hit
    user2, err := userService.GetUser(ctx, "123")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Second call: %+v\n", user2)
}

Debugging Demonstration

// Debugging and monitoring utilities
func (r *RedisService) DebugInfo() {
    info, err := r.client.Info(r.ctx).Result()
    if err != nil {
        log.Printf("Failed to get Redis info: %v", err)
        return
    }
    fmt.Println("Redis Info:", info)
}

func (r *RedisService) MonitorKeys(pattern string) {
    keys, err := r.client.Keys(r.ctx, pattern).Result()
    if err != nil {
        log.Printf("Failed to get keys: %v", err)
        return
    }
    
    for _, key := range keys {
        ttl, _ := r.client.TTL(r.ctx, key).Result()
        keyType, _ := r.client.Type(r.ctx, key).Result()
        fmt.Printf("Key: %s, Type: %s, TTL: %v\n", key, keyType, ttl)
    }
}

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

Exercise 1: User Session Management (15 minutes)

Objective: Build a complete session management system using Go and Redis

Instructions:

  1. Create a session structure with user ID, username, and expiration
  2. Implement session creation, retrieval, and deletion
  3. Add session timeout handling
  4. Include proper error handling and logging

Starter Code:

package main

import (
    "context"
    "encoding/json"
    "time"
    "github.com/redis/go-redis/v9"
)

type Session struct {
    UserID    int       `json:"user_id"`
    Username  string    `json:"username"`
    CreatedAt time.Time `json:"created_at"`
    ExpiresAt time.Time `json:"expires_at"`
}

type SessionManager struct {
    client *redis.Client
    ctx    context.Context
}

func NewSessionManager(client *redis.Client) *SessionManager {
    // TODO: Initialize session manager
}

func (s *SessionManager) CreateSession(userID int, username string) (string, error) {
    // TODO: Create a new session with unique ID
    // TODO: Store session in Redis with expiration
    // TODO: Return session ID
}

func (s *SessionManager) GetSession(sessionID string) (*Session, error) {
    // TODO: Retrieve session from Redis
    // TODO: Handle session not found
    // TODO: Unmarshal and return session
}

func (s *SessionManager) DeleteSession(sessionID string) error {
    // TODO: Remove session from Redis
}

func (s *SessionManager) RefreshSession(sessionID string) error {
    // TODO: Extend session expiration
}

func main() {
    // TODO: Set up Redis client
    // TODO: Create session manager
    // TODO: Test session operations
}

Solution:

package main

import (
    "context"
    "crypto/rand"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "log"
    "time"

    "github.com/redis/go-redis/v9"
)

type Session struct {
    UserID    int       `json:"user_id"`
    Username  string    `json:"username"`
    CreatedAt time.Time `json:"created_at"`
    ExpiresAt time.Time `json:"expires_at"`
}

type SessionManager struct {
    client *redis.Client
    ctx    context.Context
    ttl    time.Duration
}

func NewSessionManager(client *redis.Client) *SessionManager {
    return &SessionManager{
        client: client,
        ctx:    context.Background(),
        ttl:    30 * time.Minute,
    }
}

func (s *SessionManager) generateSessionID() (string, error) {
    bytes := make([]byte, 16)
    if _, err := rand.Read(bytes); err != nil {
        return "", err
    }
    return hex.EncodeToString(bytes), nil
}

func (s *SessionManager) CreateSession(userID int, username string) (string, error) {
    sessionID, err := s.generateSessionID()
    if err != nil {
        return "", fmt.Errorf("failed to generate session ID: %w", err)
    }

    session := Session{
        UserID:    userID,
        Username:  username,
        CreatedAt: time.Now(),
        ExpiresAt: time.Now().Add(s.ttl),
    }

    sessionData, err := json.Marshal(session)
    if err != nil {
        return "", fmt.Errorf("failed to marshal session: %w", err)
    }

    key := fmt.Sprintf("session:%s", sessionID)
    err = s.client.Set(s.ctx, key, sessionData, s.ttl).Err()
    if err != nil {
        return "", fmt.Errorf("failed to store session: %w", err)
    }

    return sessionID, nil
}

func (s *SessionManager) GetSession(sessionID string) (*Session, error) {
    key := fmt.Sprintf("session:%s", sessionID)
    
    val, err := s.client.Get(s.ctx, key).Result()
    if err == redis.Nil {
        return nil, fmt.Errorf("session not found")
    } else if err != nil {
        return nil, fmt.Errorf("failed to get session: %w", err)
    }

    var session Session
    if err := json.Unmarshal([]byte(val), &session); err != nil {
        return nil, fmt.Errorf("failed to unmarshal session: %w", err)
    }

    return &session, nil
}

func (s *SessionManager) DeleteSession(sessionID string) error {
    key := fmt.Sprintf("session:%s", sessionID)
    return s.client.Del(s.ctx, key).Err()
}

func (s *SessionManager) RefreshSession(sessionID string) error {
    key := fmt.Sprintf("session:%s", sessionID)
    return s.client.Expire(s.ctx, key, s.ttl).Err()
}

func main() {
    client := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })
    defer client.Close()

    sessionMgr := NewSessionManager(client)

    // Create session
    sessionID, err := sessionMgr.CreateSession(123, "john_doe")
    if err != nil {
        log.Fatal("Failed to create session:", err)
    }
    fmt.Printf("Created session: %s\n", sessionID)

    // Get session
    session, err := sessionMgr.GetSession(sessionID)
    if err != nil {
        log.Fatal("Failed to get session:", err)
    }
    fmt.Printf("Retrieved session: %+v\n", session)

    // Refresh session
    err = sessionMgr.RefreshSession(sessionID)
    if err != nil {
        log.Fatal("Failed to refresh session:", err)
    }
    fmt.Println("Session refreshed")

    // Delete session
    err = sessionMgr.DeleteSession(sessionID)
    if err != nil {
        log.Fatal("Failed to delete session:", err)
    }
    fmt.Println("Session deleted")
}

Exercise 2: Rate Limiting Implementation (15 minutes)

Objective: Create a rate limiting system using Redis and Go

Requirements:

  • Implement sliding window rate limiting
  • Support different limits for different users/endpoints
  • Provide remaining request count
  • Handle cleanup of expired counters

Challenge Implementation:

package main

import (
    "context"
    "fmt"
    "log"
    "strconv"
    "time"

    "github.com/redis/go-redis/v9"
)

type RateLimiter struct {
    client *redis.Client
    ctx    context.Context
}

func NewRateLimiter(client *redis.Client) *RateLimiter {
    return &RateLimiter{
        client: client,
        ctx:    context.Background(),
    }
}

// IsAllowed checks if a request is within rate limits
func (r *RateLimiter) IsAllowed(identifier string, limit int, window time.Duration) (bool, int, error) {
    now := time.Now().Unix()
    windowStart := now - int64(window.Seconds())
    
    pipe := r.client.Pipeline()
    
    // Remove old entries
    pipe.ZRemRangeByScore(r.ctx, identifier, "0", strconv.FormatInt(windowStart, 10))
    
    // Count current requests
    countCmd := pipe.ZCard(r.ctx, identifier)
    
    // Add current request
    pipe.ZAdd(r.ctx, identifier, redis.Z{Score: float64(now), Member: now})
    
    // Set expiration
    pipe.Expire(r.ctx, identifier, window)
    
    _, err := pipe.Exec(r.ctx)
    if err != nil {
        return false, 0, err
    }
    
    currentCount := int(countCmd.Val())
    remaining := limit - currentCount - 1
    
    if currentCount >= limit {
        // Remove the request we just added since it's rejected
        r.client.ZRem(r.ctx, identifier, now)
        return false, 0, nil
    }
    
    return true, remaining, nil
}

func main() {
    client := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })
    defer client.Close()

    limiter := NewRateLimiter(client)

    // Test rate limiting: 5 requests per minute
    userID := "user:123"
    limit := 5
    window := time.Minute

    for i := 0; i < 7; i++ {
        allowed, remaining, err := limiter.IsAllowed(userID, limit, window)
        if err != nil {
            log.Printf("Error checking rate limit: %v", err)
            continue
        }

        if allowed {
            fmt.Printf("Request %d: ALLOWED (remaining: %d)\n", i+1, remaining)
        } else {
            fmt.Printf("Request %d: REJECTED (rate limit exceeded)\n", i+1)
        }

        time.Sleep(100 * time.Millisecond) // Small delay between requests
    }
}

Exercise 3: Real-time Leaderboard (10 minutes)

Objective: Build a gaming leaderboard with real-time updates

Features to Implement:

  • Add/update player scores
  • Get top N players
  • Get player rank and neighbors
  • Support multiple leaderboards

Implementation:

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/redis/go-redis/v9"
)

type Leaderboard struct {
    client *redis.Client
    ctx    context.Context
    name   string
}

func NewLeaderboard(client *redis.Client, name string) *Leaderboard {
    return &Leaderboard{
        client: client,
        ctx:    context.Background(),
        name:   fmt.Sprintf("leaderboard:%s", name),
    }
}

type Player struct {
    Username string
    Score    float64
    Rank     int64
}

func (l *Leaderboard) UpdateScore(username string, score float64) error {
    return l.client.ZAdd(l.ctx, l.name, redis.Z{
        Score:  score,
        Member: username,
    }).Err()
}

func (l *Leaderboard) GetTopPlayers(count int) ([]*Player, error) {
    result, err := l.client.ZRevRangeWithScores(l.ctx, l.name, 0, int64(count-1)).Result()
    if err != nil {
        return nil, err
    }

    players := make([]*Player, len(result))
    for i, z := range result {
        players[i] = &Player{
            Username: z.Member.(string),
            Score:    z.Score,
            Rank:     int64(i + 1),
        }
    }

    return players, nil
}

func (l *Leaderboard) GetPlayerRank(username string) (*Player, error) {
    rank, err := l.client.ZRevRank(l.ctx, l.name, username).Result()
    if err == redis.Nil {
        return nil, fmt.Errorf("player not found")
    } else if err != nil {
        return nil, err
    }

    score, err := l.client.ZScore(l.ctx, l.name, username).Result()
    if err != nil {
        return nil, err
    }

    return &Player{
        Username: username,
        Score:    score,
        Rank:     rank + 1, // Redis uses 0-based ranking
    }, nil
}

func main() {
    client := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })
    defer client.Close()

    leaderboard := NewLeaderboard(client, "global")

    // Add some players
    players := map[string]float64{
        "alice":   1500,
        "bob":     1200,
        "charlie": 1800,
        "diana":   1600,
        "eve":     1400,
    }

    for username, score := range players {
        err := leaderboard.UpdateScore(username, score)
        if err != nil {
            log.Printf("Failed to update score for %s: %v", username, err)
        }
    }

    // Get top 3 players
    topPlayers, err := leaderboard.GetTopPlayers(3)
    if err != nil {
        log.Fatal("Failed to get top players:", err)
    }

    fmt.Println("Top 3 Players:")
    for _, player := range topPlayers {
        fmt.Printf("%d. %s - %.0f points\n", player.Rank, player.Username, player.Score)
    }

    // Get specific player rank
    player, err := leaderboard.GetPlayerRank("alice")
    if err != nil {
        log.Printf("Failed to get alice's rank: %v", err)
    } else {
        fmt.Printf("\nAlice's rank: %d with %.0f points\n", player.Rank, player.Score)
    }
}

7. Common Pitfalls and Troubleshooting (10 minutes)

Frequent Mistakes

  1. Problem: Not handling redis.Nil errors properly Example: Treating key-not-found as a critical error Solution: Check for redis.Nil explicitly and handle appropriately

    val, err := client.Get(ctx, key).Result()
    if err == redis.Nil {
        // Key doesn't exist - handle gracefully
        return defaultValue, nil
    } else if err != nil {
        // Actual error - handle as error
        return "", err
    }
    
  2. Problem: Not using context for timeout control Cause: Missing context in Redis operations Solution: Always pass context to Redis operations

    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    result, err := client.Get(ctx, key).Result()
    
  3. Problem: Memory leaks from unclosed clients Cause: Forgetting to call client.Close() Solution: Always defer client closure

    client := redis.NewClient(options)
    defer client.Close()
    
  4. Problem: JSON marshaling/unmarshaling errors Cause: Struct tags or invalid JSON data Solution: Proper error handling and struct validation

    data, err := json.Marshal(obj)
    if err != nil {
        return fmt.Errorf("marshal error: %w", err)
    }
    

Debugging Tips

// Enable debug logging
redis.SetLogger(log.New(os.Stdout, "redis: ", log.LstdFlags))

// Check connection status
pong, err := client.Ping(ctx).Result()
if err != nil {
    log.Printf("Redis connection failed: %v", err)
}

// Monitor Redis commands (development only)
client.AddHook(redis.NewLoggingHook())

// Check client statistics
stats := client.PoolStats()
fmt.Printf("Pool stats: %+v\n", stats)

Performance Considerations

  • Use connection pooling appropriately (default settings are usually good)
  • Batch operations with pipelines when possible
  • Use appropriate timeouts for different operations
  • Monitor memory usage and connection counts
  • Consider Redis Cluster for horizontal scaling

8. Best Practices and Professional Tips (5 minutes)

Industry Standards

  • Error Handling: Always handle redis.Nil separately from other errors
  • Context Usage: Use context for all operations with appropriate timeouts
  • Connection Management: Use a single client instance with connection pooling
  • Key Naming: Use consistent, hierarchical key naming conventions
  • Serialization: Choose between JSON, MessagePack, or Protocol Buffers based on performance needs

Code Organization

// Recommended project structure
/internal
  /redis
    client.go      // Redis client setup and configuration
    operations.go  // Common Redis operations
  /cache
    manager.go     // High-level cache management
    models.go      // Cache data models
  /services
    user.go        // Business logic with caching
/cmd
  main.go          // Application entry point

Production Configuration

func NewProductionRedisClient() *redis.Client {
    return redis.NewClient(&redis.Options{
        Addr:         os.Getenv("REDIS_URL"),
        Password:     os.Getenv("REDIS_PASSWORD"),
        DB:           0,
        PoolSize:     20,
        MinIdleConns: 10,
        MaxRetries:   3,
        DialTimeout:  5 * time.Second,
        ReadTimeout:  3 * time.Second,
        WriteTimeout: 3 * time.Second,
        IdleTimeout:  5 * time.Minute,
    })
}

Next Steps in Your Learning Journey

  • Explore Redis pub/sub for real-time features
  • Learn Redis transactions and Lua scripting
  • Study Redis clustering and high availability
  • Master Redis persistence and backup strategies
  • Investigate Redis modules and extensions

9. Summary and Review (5 minutes)

Key Takeaways

  1. Go-redis provides excellent Redis integration with type safety and Go idioms
  2. Proper error handling distinguishes between different error types (nil, timeout, network)
  3. Context usage is essential for timeout control and cancellation
  4. Connection pooling handles concurrency automatically with proper configuration
  5. JSON serialization makes it easy to store complex Go structures in Redis

Quick Review Questions

  1. How do you handle the case when a Redis key doesn’t exist?
  2. What’s the purpose of using context in Redis operations?
  3. How would you implement retry logic for Redis operations?
  4. What’s the difference between SET and SETEX operations in go-redis?
  5. How do you properly close a Redis client connection?

Code Snippet Cheat Sheet

// Basic setup
client := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
defer client.Close()

// Context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

// Error handling
val, err := client.Get(ctx, key).Result()
if err == redis.Nil {
    // Key doesn't exist
} else if err != nil {
    // Real error
}

// JSON operations
json.Marshal(obj) -> client.Set()
client.Get() -> json.Unmarshal()

// Pipeline for batch operations
pipe := client.Pipeline()
pipe.Set(ctx, "key1", "val1", 0)
pipe.Set(ctx, "key2", "val2", 0)
pipe.Exec(ctx)

For Instructors

Teaching Tips

  • Start with simple string operations before moving to complex data structures
  • Emphasize error handling patterns early, as they’re different from typical Go errors
  • Use live coding to show the debugging process with Redis
  • Demonstrate both successful operations and common error scenarios
  • Show performance differences between individual operations and pipelines

Common Student Questions

  • Q: “Why does redis.Nil exist when Go has standard nil?” A: redis.Nil indicates a key doesn’t exist, which is different from a network error or other failures

  • Q: “Should I create a new client for each request?” A: No, use a single client with connection pooling for better performance and resource management

  • Q: “How do I know when to use pipelines?” A: Use pipelines when you need to execute multiple Redis commands atomically or want to reduce network round trips

Assessment Ideas

  • Build a complete caching layer for a web application
  • Implement session management with proper expiration handling
  • Create a rate limiting middleware using Redis
  • Design a real-time feature using Redis data structures
  • Debug intentionally broken Redis integration code

Adaptation Notes

  • Beginners: Focus on basic operations and error handling before advanced patterns
  • Advanced students: Cover pipelines, Lua scripting, and performance optimization
  • Time constraints: Prioritize connection setup, basic operations, and error handling
  • Online delivery: Provide pre-configured Redis instances and starter code templates