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:
- Go-Redis Library Setup - Installing and configuring the go-redis client
- Connection Management - Connection pooling, timeouts, and error handling
- Basic Operations - Implementing string, hash, list, set, and sorted set operations
- Error Handling - Graceful error handling and retry mechanisms
- Best Practices - Code organization, testing, and performance considerations
- 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
- Type Safety: Compile-time checking for Redis operations
- Connection Pooling: Automatic connection management for high concurrency
- Pipeline Support: Batch operations for better performance
- Pub/Sub: Real-time messaging capabilities
- Cluster Support: Distributed Redis deployments
- 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
-
Misconception: “I need to create a new client for each operation” Reality: Use a single client instance with connection pooling for better performance
-
Misconception: “Context is optional in go-redis” Reality: Always use context for timeout control and cancellation
-
Misconception: “Error handling is the same as other Go libraries” Reality: Redis has specific error types like
redis.Nilthat 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:
- Create a session structure with user ID, username, and expiration
- Implement session creation, retrieval, and deletion
- Add session timeout handling
- 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
-
Problem: Not handling
redis.Nilerrors properly Example: Treating key-not-found as a critical error Solution: Check forredis.Nilexplicitly and handle appropriatelyval, 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 } -
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() -
Problem: Memory leaks from unclosed clients Cause: Forgetting to call
client.Close()Solution: Always defer client closureclient := redis.NewClient(options) defer client.Close() -
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.Nilseparately 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
- Go-redis provides excellent Redis integration with type safety and Go idioms
- Proper error handling distinguishes between different error types (nil, timeout, network)
- Context usage is essential for timeout control and cancellation
- Connection pooling handles concurrency automatically with proper configuration
- JSON serialization makes it easy to store complex Go structures in Redis
Quick Review Questions
- How do you handle the case when a Redis key doesn’t exist?
- What’s the purpose of using context in Redis operations?
- How would you implement retry logic for Redis operations?
- What’s the difference between
SETandSETEXoperations in go-redis? - 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.Nilexist when Go has standard nil?” A:redis.Nilindicates 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