Skip to main content

Redis Integration — Caching and Session Management with go-redis

Redis is an in-memory data structure store widely used in Go applications for caching, session management, Pub/Sub, and Rate Limiting.


go-redis Installation

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

Redis Connection

package main

import (
"context"
"fmt"
"log"

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

func newRedisClient() *redis.Client {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // empty if no password
DB: 0, // default DB number
PoolSize: 10, // connection pool size
})
return rdb
}

func main() {
ctx := context.Background()
rdb := newRedisClient()
defer rdb.Close()

// Verify connection
pong, err := rdb.Ping(ctx).Result()
if err != nil {
log.Fatal("Redis connection failed:", err)
}
fmt.Println("Redis response:", pong) // PONG
}

Redis Cluster Connection

func newRedisCluster() *redis.ClusterClient {
return redis.NewClusterClient(&redis.ClusterOptions{
Addrs: []string{
"localhost:7000",
"localhost:7001",
"localhost:7002",
},
})
}

Basic String Commands

func stringExamples(ctx context.Context, rdb *redis.Client) {
// SET
err := rdb.Set(ctx, "username", "golang", 0).Err() // 0 = no expiration
if err != nil {
log.Fatal(err)
}

// SET with TTL (Time To Live)
err = rdb.Set(ctx, "token", "abc123", 30*time.Minute).Err()
if err != nil {
log.Fatal(err)
}

// GET
val, err := rdb.Get(ctx, "username").Result()
if err == redis.Nil {
fmt.Println("Key not found")
} else if err != nil {
log.Fatal(err)
} else {
fmt.Println("username:", val)
}

// GETSET (get value and replace with new one)
old, _ := rdb.GetSet(ctx, "username", "new_golang").Result()
fmt.Println("Old value:", old)

// MSET / MGET (multiple keys)
rdb.MSet(ctx, "key1", "val1", "key2", "val2", "key3", "val3")
vals, _ := rdb.MGet(ctx, "key1", "key2", "key3").Result()
fmt.Println("Multiple get:", vals)

// INCR / INCRBY
rdb.Set(ctx, "counter", 10, 0)
rdb.Incr(ctx, "counter") // 11
rdb.IncrBy(ctx, "counter", 5) // 16
count, _ := rdb.Get(ctx, "counter").Int()
fmt.Println("Counter:", count)

// EXISTS
exists, _ := rdb.Exists(ctx, "username").Result()
fmt.Println("Exists:", exists) // 1

// DEL
rdb.Del(ctx, "username", "token")

// EXPIRE
rdb.Expire(ctx, "counter", 1*time.Hour)

// TTL query
ttl, _ := rdb.TTL(ctx, "counter").Result()
fmt.Println("Remaining TTL:", ttl)
}

Caching Pattern — Cache-Aside

The most common caching strategy: "load from DB if not in cache, then store in cache".

package cache

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

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

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

type UserCache struct {
rdb *redis.Client
ttl time.Duration
}

func NewUserCache(rdb *redis.Client, ttl time.Duration) *UserCache {
return &UserCache{rdb: rdb, ttl: ttl}
}

func (c *UserCache) cacheKey(id int64) string {
return fmt.Sprintf("user:%d", id)
}

// Cache-Aside pattern: check cache → query DB if miss → store in cache
func (c *UserCache) GetUser(ctx context.Context, id int64, fetchFromDB func(int64) (*User, error)) (*User, error) {
key := c.cacheKey(id)

// 1. Check cache
data, err := c.rdb.Get(ctx, key).Bytes()
if err == nil {
// Cache hit
var user User
if err := json.Unmarshal(data, &user); err == nil {
return &user, nil
}
}

if err != redis.Nil {
// Cache error (Redis down, etc.) → query DB directly (graceful degradation)
fmt.Printf("Cache query failed (fallback to DB): %v\n", err)
}

// 2. Cache miss → query DB
user, err := fetchFromDB(id)
if err != nil {
return nil, fmt.Errorf("DB query failed: %w", err)
}
if user == nil {
return nil, nil
}

// 3. Store in cache
if data, err := json.Marshal(user); err == nil {
c.rdb.Set(ctx, key, data, c.ttl) // ignore cache failure (data still returned)
}

return user, nil
}

// Invalidate cache
func (c *UserCache) InvalidateUser(ctx context.Context, id int64) error {
return c.rdb.Del(ctx, c.cacheKey(id)).Err()
}

Hash — Storing Structs

Redis Hash allows field-level reads and writes, suitable for storing structs.

func hashExamples(ctx context.Context, rdb *redis.Client) {
// HSET: save multiple fields at once
rdb.HSet(ctx, "user:1001", map[string]interface{}{
"name": "Kim Golang",
"email": "golang@example.com",
"age": "30",
})

// HGET: single field query
name, _ := rdb.HGet(ctx, "user:1001", "name").Result()
fmt.Println("Name:", name)

// HGETALL: query all fields
fields, _ := rdb.HGetAll(ctx, "user:1001").Result()
fmt.Println("All:", fields)

// HMGET: multiple fields query
vals, _ := rdb.HMGet(ctx, "user:1001", "name", "email").Result()
fmt.Println("Name, email:", vals)

// HINCRBY: increment numeric field
rdb.HIncrBy(ctx, "user:1001", "login_count", 1)

// HDEL: delete field
rdb.HDel(ctx, "user:1001", "age")

// HEXISTS: check field existence
exists, _ := rdb.HExists(ctx, "user:1001", "name").Result()
fmt.Println("name exists:", exists)
}

List — Task Queue

func listExamples(ctx context.Context, rdb *redis.Client) {
queueKey := "task:queue"

// RPUSH: push to right (tail)
rdb.RPush(ctx, queueKey, "task1", "task2", "task3")

// LPOP: pop from left (head) → FIFO queue
task, err := rdb.LPop(ctx, queueKey).Result()
if err == redis.Nil {
fmt.Println("Queue empty")
} else {
fmt.Println("Task to process:", task)
}

// BLPOP: blocking pop (wait until available, with timeout)
result, err := rdb.BLPop(ctx, 5*time.Second, queueKey).Result()
if err == redis.Nil {
fmt.Println("Timeout: queue empty")
} else if err == nil {
fmt.Printf("Blocking pop: queue=%s, task=%s\n", result[0], result[1])
}

// LLEN: length
length, _ := rdb.LLen(ctx, queueKey).Result()
fmt.Println("Queue length:", length)
}

Set — Unique Collection

func setExamples(ctx context.Context, rdb *redis.Client) {
// SADD: add members
rdb.SAdd(ctx, "online:users", "user:1", "user:2", "user:3")
rdb.SAdd(ctx, "vip:users", "user:2", "user:3", "user:4")

// SMEMBERS: all members
members, _ := rdb.SMembers(ctx, "online:users").Result()
fmt.Println("Online users:", members)

// SISMEMBER: check membership
isOnline, _ := rdb.SIsMember(ctx, "online:users", "user:1").Result()
fmt.Println("user:1 online:", isOnline)

// SINTER: intersection (online VIPs)
vipOnline, _ := rdb.SInter(ctx, "online:users", "vip:users").Result()
fmt.Println("Online VIPs:", vipOnline)

// SCARD: set size
count, _ := rdb.SCard(ctx, "online:users").Result()
fmt.Println("Online count:", count)
}

Session Management

package session

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

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

type Session struct {
UserID int64 `json:"user_id"`
Username string `json:"username"`
Role string `json:"role"`
CreatedAt int64 `json:"created_at"`
}

type SessionStore struct {
rdb *redis.Client
ttl time.Duration
}

func NewSessionStore(rdb *redis.Client, ttl time.Duration) *SessionStore {
return &SessionStore{rdb: rdb, ttl: ttl}
}

func (s *SessionStore) Create(ctx context.Context, sess Session) (string, error) {
sessionID := uuid.New().String()
key := fmt.Sprintf("session:%s", sessionID)

data, err := json.Marshal(sess)
if err != nil {
return "", err
}

if err := s.rdb.Set(ctx, key, data, s.ttl).Err(); err != nil {
return "", fmt.Errorf("save session failed: %w", err)
}
return sessionID, nil
}

func (s *SessionStore) Get(ctx context.Context, sessionID string) (*Session, error) {
key := fmt.Sprintf("session:%s", sessionID)

data, err := s.rdb.Get(ctx, key).Bytes()
if err == redis.Nil {
return nil, nil // session expired or not found
}
if err != nil {
return nil, err
}

var sess Session
return &sess, json.Unmarshal(data, &sess)
}

func (s *SessionStore) Refresh(ctx context.Context, sessionID string) error {
key := fmt.Sprintf("session:%s", sessionID)
return s.rdb.Expire(ctx, key, s.ttl).Err()
}

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

Pub/Sub — Real-time Message Broadcasting

func pubSubExample(rdb *redis.Client) {
ctx := context.Background()

// Subscriber (separate goroutine)
go func() {
pubsub := rdb.Subscribe(ctx, "notifications", "alerts")
defer pubsub.Close()

ch := pubsub.Channel()
for msg := range ch {
fmt.Printf("[Received] channel=%s, message=%s\n", msg.Channel, msg.Payload)
}
}()

time.Sleep(100 * time.Millisecond) // wait for subscriber ready

// Publisher
rdb.Publish(ctx, "notifications", "New comment posted")
rdb.Publish(ctx, "alerts", "System maintenance scheduled")

time.Sleep(200 * time.Millisecond)
}

Rate Limiting — Sliding Window

func isRateLimited(ctx context.Context, rdb *redis.Client, userID string, limit int, window time.Duration) (bool, error) {
key := fmt.Sprintf("ratelimit:%s", userID)
now := time.Now()

pipe := rdb.TxPipeline()
// Add request at current time
pipe.ZAdd(ctx, key, redis.Z{Score: float64(now.UnixNano()), Member: now.UnixNano()})
// Remove requests older than window
pipe.ZRemRangeByScore(ctx, key, "-inf", fmt.Sprintf("%d", now.Add(-window).UnixNano()))
// Get current request count
countCmd := pipe.ZCard(ctx, key)
// Set key expiration
pipe.Expire(ctx, key, window)

if _, err := pipe.Exec(ctx); err != nil {
return false, err
}

count, _ := countCmd.Result()
return count > int64(limit), nil
}

Pipelining — Batch Command Optimization

func pipelineExample(ctx context.Context, rdb *redis.Client) {
pipe := rdb.Pipeline()

// Execute multiple commands in one network round-trip
setCmd := pipe.Set(ctx, "key1", "val1", 0)
getCmd := pipe.Get(ctx, "key1")
incrCmd := pipe.Incr(ctx, "counter")

_, err := pipe.Exec(ctx)
if err != nil {
log.Fatal(err)
}

fmt.Println("SET:", setCmd.Err())
fmt.Println("GET:", getCmd.Val())
fmt.Println("INCR:", incrCmd.Val())
}

Key Summary

Data StructureUse Cases
StringCache, counters, session tokens
HashUser profiles, settings
ListTask queues, recent activity
SetOnline users, tags
Sorted SetLeaderboards, Rate Limiting
  • Design for DB fallback even when Redis is unavailable
  • Use namespace prefixes for cache keys (user:, session:) to prevent collisions
  • Always set expiration times; without TTL, memory will be exhausted