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 Structure | Use Cases |
|---|---|
| String | Cache, counters, session tokens |
| Hash | User profiles, settings |
| List | Task queues, recent activity |
| Set | Online users, tags |
| Sorted Set | Leaderboards, 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