turash/dev_guides/07_redis_go.md
Damir Mukimov 4a2fda96cd
Initial commit: Repository setup with .gitignore, golangci-lint v2.6.0, and code quality checks
- Initialize git repository
- Add comprehensive .gitignore for Go projects
- Install golangci-lint v2.6.0 (latest v2) globally
- Configure .golangci.yml with appropriate linters and formatters
- Fix all formatting issues (gofmt)
- Fix all errcheck issues (unchecked errors)
- Adjust complexity threshold for validation functions
- All checks passing: build, test, vet, lint
2025-11-01 07:36:22 +01:00

9.6 KiB

Redis Go Client Development Guide

Library: github.com/redis/go-redis/v9
Used In: MVP - Caching, sessions, match results
Purpose: Redis client for caching and real-time data


Where It's Used

  • Match results caching (TTL: 5-15 minutes)
  • Session management
  • Rate limiting
  • Pub/sub for real-time updates (future)

Official Documentation


Installation

go get github.com/redis/go-redis/v9

Key Concepts

1. Client Setup

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

func NewRedisClient(addr, password string, db int) (*redis.Client, error) {
    rdb := redis.NewClient(&redis.Options{
        Addr:     addr,     // "localhost:6379"
        Password: password, // "" for no password
        DB:       db,       // 0
    })
    
    // Verify connection
    ctx := context.Background()
    if err := rdb.Ping(ctx).Err(); err != nil {
        return nil, err
    }
    
    return rdb, nil
}

2. Basic Operations

ctx := context.Background()

// Set value
err := rdb.Set(ctx, "key", "value", 0).Err()

// Set with expiration
err := rdb.Set(ctx, "key", "value", 5*time.Minute).Err()

// Get value
val, err := rdb.Get(ctx, "key").Result()
if err == redis.Nil {
    // Key doesn't exist
}

// Delete
err := rdb.Del(ctx, "key").Err()

// Check if key exists
exists, err := rdb.Exists(ctx, "key").Result()

// Set if not exists (NX)
err := rdb.SetNX(ctx, "key", "value", time.Minute).Err()

3. Hash Operations

// Set hash field
err := rdb.HSet(ctx, "business:123", "name", "Factory A").Err()

// Set multiple hash fields
err := rdb.HSet(ctx, "business:123", map[string]interface{}{
    "name":  "Factory A",
    "email": "contact@factorya.com",
}).Err()

// Get hash field
name, err := rdb.HGet(ctx, "business:123", "name").Result()

// Get all hash fields
business, err := rdb.HGetAll(ctx, "business:123").Result()

// Increment hash field
count, err := rdb.HIncrBy(ctx, "business:123", "match_count", 1).Result()

4. List Operations

// Push to list
err := rdb.LPush(ctx, "matches:123", "match1").Err()

// Pop from list
match, err := rdb.RPop(ctx, "matches:123").Result()

// Get list length
length, err := rdb.LLen(ctx, "matches:123").Result()

// Get list range
matches, err := rdb.LRange(ctx, "matches:123", 0, 10).Result()

5. Set Operations

// Add to set
err := rdb.SAdd(ctx, "businesses", "business:123").Err()

// Check if member exists
exists, err := rdb.SIsMember(ctx, "businesses", "business:123").Result()

// Get set members
members, err := rdb.SMembers(ctx, "businesses").Result()

// Remove from set
err := rdb.SRem(ctx, "businesses", "business:123").Err()

6. Sorted Sets (for ranking)

// Add to sorted set
err := rdb.ZAdd(ctx, "match_scores", redis.Z{
    Score:  95.5,
    Member: "match:123",
}).Err()

// Get top matches (by score)
matches, err := rdb.ZRevRange(ctx, "match_scores", 0, 9).Result()

// Get with scores
matchesWithScores, err := rdb.ZRevRangeWithScores(ctx, "match_scores", 0, 9).Result()

7. JSON Operations

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

// Set JSON
err := rdb.JSONSet(ctx, "business:123", "$", business).Err()

// Get JSON
var business Business
err := rdb.JSONGet(ctx, "business:123", "$").Scan(&business)

MVP-Specific Patterns

Match Results Caching

type MatchCache struct {
    client *redis.Client
}

// Cache match results with TTL
func (c *MatchCache) CacheMatches(ctx context.Context, flowID string, matches []Match, ttl time.Duration) error {
    key := fmt.Sprintf("matches:%s", flowID)
    
    // Serialize matches
    data, err := json.Marshal(matches)
    if err != nil {
        return err
    }
    
    return c.client.Set(ctx, key, data, ttl).Err()
}

// Get cached matches
func (c *MatchCache) GetMatches(ctx context.Context, flowID string) ([]Match, error) {
    key := fmt.Sprintf("matches:%s", flowID)
    
    data, err := c.client.Get(ctx, key).Bytes()
    if err == redis.Nil {
        return nil, ErrCacheMiss
    }
    if err != nil {
        return nil, err
    }
    
    var matches []Match
    if err := json.Unmarshal(data, &matches); err != nil {
        return nil, err
    }
    
    return matches, nil
}

// Invalidate cache
func (c *MatchCache) Invalidate(ctx context.Context, flowID string) error {
    key := fmt.Sprintf("matches:%s", flowID)
    return c.client.Del(ctx, key).Err()
}

// Invalidate pattern (all matches for a business)
func (c *MatchCache) InvalidatePattern(ctx context.Context, pattern string) error {
    keys, err := c.client.Keys(ctx, pattern).Result()
    if err != nil {
        return err
    }
    
    if len(keys) > 0 {
        return c.client.Del(ctx, keys...).Err()
    }
    
    return nil
}

Session Management

type SessionManager struct {
    client *redis.Client
}

func (s *SessionManager) CreateSession(ctx context.Context, userID string) (string, error) {
    sessionID := uuid.New().String()
    key := fmt.Sprintf("session:%s", sessionID)
    
    err := s.client.Set(ctx, key, userID, 24*time.Hour).Err()
    if err != nil {
        return "", err
    }
    
    return sessionID, nil
}

func (s *SessionManager) GetSession(ctx context.Context, sessionID string) (string, error) {
    key := fmt.Sprintf("session:%s", sessionID)
    userID, err := s.client.Get(ctx, key).Result()
    if err == redis.Nil {
        return "", ErrSessionNotFound
    }
    return userID, err
}

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

Rate Limiting

type RateLimiter struct {
    client *redis.Client
}

func (r *RateLimiter) Allow(ctx context.Context, key string, limit int, window time.Duration) (bool, error) {
    // Sliding window rate limiting
    now := time.Now().Unix()
    windowStart := now - int64(window.Seconds())
    
    pipe := r.client.Pipeline()
    
    // Remove old entries
    pipe.ZRemRangeByScore(ctx, key, "0", strconv.FormatInt(windowStart, 10))
    
    // Count current requests
    pipe.ZCard(ctx, key)
    
    // Add current request
    pipe.ZAdd(ctx, key, redis.Z{
        Score:  float64(now),
        Member: uuid.New().String(),
    })
    
    // Set expiry
    pipe.Expire(ctx, key, window)
    
    results, err := pipe.Exec(ctx)
    if err != nil {
        return false, err
    }
    
    count := results[1].(*redis.IntCmd).Val()
    
    return count < int64(limit), nil
}

// Token bucket implementation
func (r *RateLimiter) TokenBucket(ctx context.Context, key string, capacity int, refillRate float64) (bool, error) {
    script := `
        local key = KEYS[1]
        local capacity = tonumber(ARGV[1])
        local tokens = tonumber(ARGV[2])
        local refillTime = tonumber(ARGV[3])
        
        local bucket = redis.call('HMGET', key, 'tokens', 'lastRefill')
        local currentTokens = tonumber(bucket[1]) or capacity
        local lastRefill = tonumber(bucket[2]) or 0
        
        local now = redis.call('TIME')
        local currentTime = tonumber(now[1])
        
        -- Refill tokens
        local elapsed = currentTime - lastRefill
        local newTokens = math.min(capacity, currentTokens + elapsed * tokens)
        
        if newTokens >= 1 then
            redis.call('HMSET', key, 'tokens', newTokens - 1, 'lastRefill', currentTime)
            redis.call('EXPIRE', key, refillTime)
            return 1
        else
            return 0
        end
    `
    
    result, err := r.client.Eval(ctx, script, []string{key}, capacity, refillRate, int(window.Seconds())).Result()
    if err != nil {
        return false, err
    }
    
    return result.(int64) == 1, nil
}

Pub/Sub (Future Use)

// Publisher
func PublishMatchUpdate(ctx context.Context, client *redis.Client, userID string, match Match) error {
    channel := fmt.Sprintf("matches:%s", userID)
    data, err := json.Marshal(match)
    if err != nil {
        return err
    }
    return client.Publish(ctx, channel, data).Err()
}

// Subscriber
func SubscribeMatchUpdates(ctx context.Context, client *redis.Client, userID string) (<-chan Match, error) {
    channel := fmt.Sprintf("matches:%s", userID)
    pubsub := client.Subscribe(ctx, channel)
    
    matches := make(chan Match)
    
    go func() {
        defer close(matches)
        for {
            msg, err := pubsub.ReceiveMessage(ctx)
            if err != nil {
                return
            }
            
            var match Match
            if err := json.Unmarshal([]byte(msg.Payload), &match); err != nil {
                continue
            }
            
            select {
            case matches <- match:
            case <-ctx.Done():
                return
            }
        }
    }()
    
    return matches, nil
}

Performance Tips

  1. Use pipelines for multiple commands
  2. Use connection pooling - configure MaxRetries, PoolSize
  3. Use pipelining - batch operations
  4. Set appropriate TTL - avoid memory leaks
  5. Use keys patterns carefully - KEYS is blocking, use SCAN for production

Error Handling

val, err := rdb.Get(ctx, "key").Result()
if err == redis.Nil {
    // Key doesn't exist
} else if err != nil {
    // Other error
    return err
}

Tutorials & Resources