Redis #

Redis (Remote Dictionary Server) is an in-memory data store that can be used as a database, cache, message broker, and session store. Its speed is remarkable — read/write operations can reach hundreds of thousands per second because all data is stored in memory. Go uses the github.com/redis/go-redis/v9 library, the most complete and actively developed Redis client.

Main Redis Data Types #

Here are Redis’s most commonly used built-in data structures and their counterparts in the go-redis library:

Data StructureShort DescriptionMain go-redis MethodsExample Use Cases
StringA single text/numeric valueGet, Set, SetNXJSON caching, session tokens, distributed locks
HashA structured key-value mapHGet, HSet, HGetAllStoring object data (e.g. user profiles)
ListAn ordered sequence of stringsLPush, RPop, BLPopTask queues, log lists
SetAn unordered collection of unique stringsSAdd, SMembers, SIsMemberDuplicate filtering, friends/followers relations
Sorted SetA unique set ordered by scoreZAdd, ZRangeByScoreBuilding leaderboards, rate limiters

Installation #

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

Connecting to Redis #

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

func newRedisClient(addr, password string, db int) *redis.Client {
    return redis.NewClient(&redis.Options{
        Addr:     addr,      // "localhost:6379"
        Password: password,  // "" if there's no password
        DB:       db,        // database index, default 0

        // Connection pool
        PoolSize:     25,
        MinIdleConns: 5,
        PoolTimeout:  30 * time.Second,

        // Timeouts
        DialTimeout:  5 * time.Second,
        ReadTimeout:  3 * time.Second,
        WriteTimeout: 3 * time.Second,

        // Retry
        MaxRetries:      3,
        MinRetryBackoff: 8 * time.Millisecond,
        MaxRetryBackoff: 512 * time.Millisecond,
    })
}

// Redis Cluster
func newClusterClient(addrs []string) *redis.ClusterClient {
    return redis.NewClusterClient(&redis.ClusterOptions{
        Addrs:    addrs,
        PoolSize: 10,
    })
}

// Redis Sentinel (High Availability)
func newSentinelClient(masterName string, sentinels []string) *redis.Client {
    return redis.NewFailoverClient(&redis.FailoverOptions{
        MasterName:    masterName,
        SentinelAddrs: sentinels,
    })
}

func main() {
    rdb := newRedisClient("localhost:6379", "", 0)
    defer rdb.Close()

    ctx := context.Background()
    if err := rdb.Ping(ctx).Err(); err != nil {
        log.Fatal("Redis ping:", err)
    }
    fmt.Println("✓ Connected to Redis")
}

String — The Most Basic Data Type #

ctx := context.Background()

// SET with a TTL
err := rdb.Set(ctx, "session:abc123", "user:42", 24*time.Hour).Err()

// GET
val, err := rdb.Get(ctx, "session:abc123").Result()
if err == redis.Nil {
    fmt.Println("Key doesn't exist")
} else if err != nil {
    log.Fatal(err)
} else {
    fmt.Println("Value:", val)
}

// SETNX — set only if the key doesn't exist
ok, err := rdb.SetNX(ctx, "lock:resource", "worker-1", 30*time.Second).Result()
if ok {
    fmt.Println("Lock acquired")
}

// GETSET — set and return the old value
old, err := rdb.GetSet(ctx, "counter", "0").Result()

// INCR / DECR — atomic increment/decrement
newVal, err := rdb.Incr(ctx, "page_views").Result()
rdb.IncrBy(ctx, "score", 10)
rdb.Decr(ctx, "stock:product:1")
rdb.DecrBy(ctx, "balance", 50000)

// MSET / MGET — many keys at once
rdb.MSet(ctx, "key1", "val1", "key2", "val2", "key3", "val3")
vals, err := rdb.MGet(ctx, "key1", "key2", "key3").Result()

// EXPIRE — set a TTL on an existing key
rdb.Expire(ctx, "session:abc123", 1*time.Hour)

// TTL — check the remaining lifetime
ttl, err := rdb.TTL(ctx, "session:abc123").Result()
fmt.Printf("TTL: %v\n", ttl) // -1 = no expiry, -2 = key doesn't exist

// EXISTS and DEL
exists, _ := rdb.Exists(ctx, "key1").Result()
rdb.Del(ctx, "key1", "key2", "key3")

Hash — A Map Within a Key #

// HSET — set one or many fields
rdb.HSet(ctx, "user:42", map[string]interface{}{
    "name":  "Budi Santoso",
    "email": "[email protected]",
    "age":   28,
    "role":  "admin",
})

// HGET — get one field
name, _ := rdb.HGet(ctx, "user:42", "name").Result()

// HMGET — get many fields
vals, _ := rdb.HMGet(ctx, "user:42", "name", "email", "role").Result()

// HGETALL — get all fields as a map
fields, _ := rdb.HGetAll(ctx, "user:42").Result()
fmt.Println(fields) // map[age:28 email:[email protected] ...]

// Scan into a struct
type User struct {
    Name  string `redis:"name"`
    Email string `redis:"email"`
    Age   int    `redis:"age"`
    Role  string `redis:"role"`
}
var user User
if err := rdb.HGetAll(ctx, "user:42").Scan(&user); err != nil {
    log.Fatal(err)
}
fmt.Printf("%+v\n", user)

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

// HDEL — delete a field
rdb.HDel(ctx, "user:42", "role")

// HEXISTS
exists, _ := rdb.HExists(ctx, "user:42", "email").Result()

List — Queues and Stacks #

// LPUSH / RPUSH — add to the left/right
rdb.LPush(ctx, "queue:jobs", "job-1", "job-2", "job-3")
rdb.RPush(ctx, "log:events", "event-a", "event-b")

// LPOP / RPOP — take from the left/right (non-blocking)
job, err := rdb.LPop(ctx, "queue:jobs").Result()

// BLPOP — blocking pop (wait until an item appears)
result, err := rdb.BLPop(ctx, 5*time.Second, "queue:jobs").Result()
// result[0] = the key name, result[1] = the value

// LRANGE — get a range
items, _ := rdb.LRange(ctx, "log:events", 0, -1).Result() // all items
recent, _ := rdb.LRange(ctx, "log:events", 0, 9).Result() // the 10 most recent

// LLEN — list length
length, _ := rdb.LLen(ctx, "queue:jobs").Result()

// LTRIM — keep only N items (sliding window log)
rdb.LTrim(ctx, "log:events", 0, 99) // keep the 100 most recent items

Set — Unique Collections #

// SADD — add members
rdb.SAdd(ctx, "online_users", "user:42", "user:99", "user:17")

// SISMEMBER — check membership
isMember, _ := rdb.SIsMember(ctx, "online_users", "user:42").Result()

// SMEMBERS — all members
members, _ := rdb.SMembers(ctx, "online_users").Result()

// SCARD — number of members
count, _ := rdb.SCard(ctx, "online_users").Result()

// SREM — remove members
rdb.SRem(ctx, "online_users", "user:42")

// Set operations
rdb.SAdd(ctx, "tags:post:1", "go", "backend", "api")
rdb.SAdd(ctx, "tags:post:2", "go", "concurrency", "goroutine")

// SINTER — intersection
common, _ := rdb.SInter(ctx, "tags:post:1", "tags:post:2").Result()
// ["go"]

// SUNION — union
all, _ := rdb.SUnion(ctx, "tags:post:1", "tags:post:2").Result()

// SDIFF — difference
diff, _ := rdb.SDiff(ctx, "tags:post:1", "tags:post:2").Result()

Sorted Set — Ordered Sets with Scores #

// ZADD — add members with scores
rdb.ZAdd(ctx, "leaderboard", redis.Z{Score: 9500, Member: "player:alice"})
rdb.ZAdd(ctx, "leaderboard", redis.Z{Score: 8200, Member: "player:budi"})
rdb.ZAdd(ctx, "leaderboard", redis.Z{Score: 9800, Member: "player:charlie"})

// ZRANGE — get by rank (ascending)
top, _ := rdb.ZRange(ctx, "leaderboard", 0, 2).Result()

// ZREVRANGE — descending (highest score first)
topPlayers, _ := rdb.ZRevRangeWithScores(ctx, "leaderboard", 0, 9).Result()
for i, p := range topPlayers {
    fmt.Printf("%d. %s: %.0f\n", i+1, p.Member, p.Score)
}

// ZRANK — a member's position (0-indexed)
rank, _ := rdb.ZRevRank(ctx, "leaderboard", "player:alice").Result()
fmt.Printf("Alice rank: %d\n", rank+1)

// ZINCRBY — add to a score
rdb.ZIncrBy(ctx, "leaderboard", 300, "player:budi")

// ZRANGEBYSCORE — filter by score range
high, _ := rdb.ZRangeByScore(ctx, "leaderboard", &redis.ZRangeBy{
    Min: "9000", Max: "+inf",
}).Result()

Pipelines — Batched Commands #

Pipelines send many commands at once, reducing round-trip overhead:

// Pipeline — no transaction, commands are sent all at once
pipe := rdb.Pipeline()
pipe.Set(ctx, "key1", "val1", time.Hour)
pipe.Set(ctx, "key2", "val2", time.Hour)
pipe.Incr(ctx, "counter")
pipe.HSet(ctx, "hash", "field", "value")

results, err := pipe.Exec(ctx)
if err != nil {
    log.Fatal(err)
}
for _, result := range results {
    if result.Err() != nil {
        log.Printf("Command error: %v", result.Err())
    }
}

// Pipelining with return values
var (
    get1 *redis.StringCmd
    get2 *redis.StringCmd
)
_, err = rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error {
    get1 = pipe.Get(ctx, "key1")
    get2 = pipe.Get(ctx, "key2")
    return nil
})
fmt.Println(get1.Val(), get2.Val())

Transactions with WATCH (Optimistic Locking) #

// WATCH + MULTI/EXEC — optimistic transactions
func transferPoints(ctx context.Context, rdb *redis.Client, from, to string, amount int64) error {
    return rdb.Watch(ctx, func(tx *redis.Tx) error {
        // Read the balance
        fromBal, err := tx.Get(ctx, "points:"+from).Int64()
        if err != nil {
            return err
        }
        if fromBal < amount {
            return errors.New("insufficient balance")
        }

        // Execute within a transaction (MULTI/EXEC)
        _, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
            pipe.DecrBy(ctx, "points:"+from, amount)
            pipe.IncrBy(ctx, "points:"+to, amount)
            return nil
        })
        return err
        // If "points:from" or "points:to" changed since WATCH,
        // the transaction fails and is retried automatically
    }, "points:"+from, "points:"+to)
}

Distributed Locks #

// Distributed lock — prevents race conditions in distributed systems
func acquireLock(ctx context.Context, rdb *redis.Client, key string, ttl time.Duration) (string, error) {
    token := uuid.New().String()

    // SET NX — only set if it doesn't exist (atomic)
    ok, err := rdb.SetNX(ctx, "lock:"+key, token, ttl).Result()
    if err != nil {
        return "", err
    }
    if !ok {
        return "", errors.New("the lock is already held by another process")
    }
    return token, nil
}

func releaseLock(ctx context.Context, rdb *redis.Client, key, token string) error {
    // Use a Lua script for a safe release (atomic check & delete)
    script := redis.NewScript(`
        if redis.call("get", KEYS[1]) == ARGV[1] then
            return redis.call("del", KEYS[1])
        else
            return 0
        end
    `)
    result, err := script.Run(ctx, rdb, []string{"lock:" + key}, token).Int()
    if err != nil {
        return err
    }
    if result == 0 {
        return errors.New("the lock has expired or is held by another process")
    }
    return nil
}

// Usage
func processWithLock(ctx context.Context, rdb *redis.Client, resourceID string) error {
    token, err := acquireLock(ctx, rdb, resourceID, 30*time.Second)
    if err != nil {
        return fmt.Errorf("could not acquire lock: %w", err)
    }
    defer releaseLock(ctx, rdb, resourceID, token)

    // Process safely — no other process can enter
    return doWork(resourceID)
}

Pub/Sub #

// Subscribe to channels
func subscribe(ctx context.Context, rdb *redis.Client, channels ...string) {
    pubsub := rdb.Subscribe(ctx, channels...)
    defer pubsub.Close()

    ch := pubsub.Channel()
    for msg := range ch {
        fmt.Printf("Channel: %s, Message: %s\n", msg.Channel, msg.Payload)
    }
}

// Publish to a channel
func publish(ctx context.Context, rdb *redis.Client, channel, message string) error {
    return rdb.Publish(ctx, channel, message).Err()
}

// Pattern subscribe
func psubscribe(ctx context.Context, rdb *redis.Client) {
    pubsub := rdb.PSubscribe(ctx, "order.*")
    defer pubsub.Close()

    for msg := range pubsub.Channel() {
        fmt.Printf("Pattern: %s, Channel: %s, Msg: %s\n",
            msg.Pattern, msg.Channel, msg.Payload)
    }
}

Caching Pattern — Cache-Aside (Lazy Loading) #

The most common caching pattern is Cache-Aside. With this pattern, the application first tries to read data from the cache. On a cache miss, the application queries the main database, stores the result in the cache with a TTL (Time-To-Live), then returns it to the client:

flowchart TD
    Req["Client Requests Data"] --> Check{"Check Redis Cache"}
    Check -->|"Cache Hit (Exists)"| Return["Return Data to Client"]
    Check -->|"Cache Miss (Not Found)"| QueryDB["Query the Main Database"]
    QueryDB --> SaveCache["Save to Redis + Set TTL"]
    SaveCache --> Return

Complete Example Program — Caching Layer #

package main

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

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

var ErrCacheMiss = errors.New("cache miss")

type Product struct {
    ID       int     `json:"id"`
    Name     string  `json:"name"`
    Price    float64 `json:"price"`
    Stock    int     `json:"stock"`
    Category string  `json:"category"`
}

// Cache — a generic caching layer
type Cache struct {
    rdb    *redis.Client
    prefix string
    ttl    time.Duration
}

func NewCache(rdb *redis.Client, prefix string, ttl time.Duration) *Cache {
    return &Cache{rdb: rdb, prefix: prefix, ttl: ttl}
}

func (c *Cache) key(id string) string {
    return c.prefix + ":" + id
}

func (c *Cache) Set(ctx context.Context, id string, value interface{}) error {
    data, err := json.Marshal(value)
    if err != nil {
        return err
    }
    return c.rdb.Set(ctx, c.key(id), data, c.ttl).Err()
}

func (c *Cache) Get(ctx context.Context, id string, dest interface{}) error {
    data, err := c.rdb.Get(ctx, c.key(id)).Bytes()
    if errors.Is(err, redis.Nil) {
        return ErrCacheMiss
    }
    if err != nil {
        return err
    }
    return json.Unmarshal(data, dest)
}

func (c *Cache) Delete(ctx context.Context, id string) error {
    return c.rdb.Del(ctx, c.key(id)).Err()
}

func (c *Cache) DeletePattern(ctx context.Context, pattern string) error {
    keys, err := c.rdb.Keys(ctx, c.prefix+":"+pattern).Result()
    if err != nil || len(keys) == 0 {
        return err
    }
    return c.rdb.Del(ctx, keys...).Err()
}

// ProductService with caching
type ProductService struct {
    cache *Cache
    // db   *sql.DB  // in production, this is the real database
}

func (s *ProductService) GetProduct(ctx context.Context, id int) (*Product, error) {
    key := fmt.Sprintf("%d", id)
    var product Product

    // Try the cache first
    if err := s.cache.Get(ctx, key, &product); err == nil {
        fmt.Printf("  [CACHE HIT] product:%d\n", id)
        return &product, nil
    }

    fmt.Printf("  [CACHE MISS] product:%d — fetching from DB\n", id)
    // Simulate fetching from the database
    product = Product{
        ID: id, Name: fmt.Sprintf("Product #%d", id),
        Price: float64(id) * 10000, Stock: 50, Category: "electronics",
    }
    time.Sleep(50 * time.Millisecond) // simulate a DB query

    // Save to the cache
    s.cache.Set(ctx, key, product)

    return &product, nil
}

func (s *ProductService) UpdateProduct(ctx context.Context, p *Product) error {
    fmt.Printf("  [DB UPDATE] product:%d\n", p.ID)
    time.Sleep(30 * time.Millisecond)

    // Invalidate the cache
    s.cache.Delete(ctx, fmt.Sprintf("%d", p.ID))
    fmt.Printf("  [CACHE INVALIDATED] product:%d\n", p.ID)
    return nil
}

// Rate Limiter using Redis
type RateLimiter struct {
    rdb *redis.Client
}

func (rl *RateLimiter) Allow(ctx context.Context, key string, limit int, window time.Duration) (bool, error) {
    pipe := rl.rdb.Pipeline()
    incr := pipe.Incr(ctx, "ratelimit:"+key)
    pipe.Expire(ctx, "ratelimit:"+key, window)

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

    count := incr.Val()
    if count > int64(limit) {
        return false, nil
    }
    return true, nil
}

func main() {
    ctx := context.Background()

    rdb := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
        DB:   0,
    })
    defer rdb.Close()

    if err := rdb.Ping(ctx).Err(); err != nil {
        log.Fatal("Redis:", err)
    }
    fmt.Println("✓ Connected to Redis\n")

    // Caching demo
    cache := NewCache(rdb, "product", 5*time.Minute)
    svc := &ProductService{cache: cache}

    fmt.Println("=== Cache Demo ===")
    for i := 0; i < 3; i++ {
        fmt.Printf("\nRequest %d for product:1:\n", i+1)
        p, err := svc.GetProduct(ctx, 1)
        if err != nil {
            log.Println(err)
        } else {
            fmt.Printf("  Result: %s (Rp%.0f)\n", p.Name, p.Price)
        }
    }

    // Update and invalidate the cache
    fmt.Println("\n=== Updating the Product ===")
    svc.UpdateProduct(ctx, &Product{ID: 1, Name: "Updated Product", Price: 99000})

    fmt.Println("\nRequest after the update:")
    p, _ := svc.GetProduct(ctx, 1)
    fmt.Printf("  Result: %s\n", p.Name)

    // Sorted Set demo — Leaderboard
    fmt.Println("\n=== Leaderboard ===")
    players := []redis.Z{
        {Score: 9800, Member: "charlie"},
        {Score: 9500, Member: "alice"},
        {Score: 8200, Member: "budi"},
        {Score: 9100, Member: "diana"},
    }
    rdb.ZAdd(ctx, "demo:leaderboard", players...)

    top, _ := rdb.ZRevRangeWithScores(ctx, "demo:leaderboard", 0, 2).Result()
    fmt.Println("Top 3:")
    for i, p := range top {
        fmt.Printf("  %d. %-10s %.0f points\n", i+1, p.Member, p.Score)
    }

    // Rate Limiter demo
    fmt.Println("\n=== Rate Limiter (5 req/10 seconds) ===")
    rl := &RateLimiter{rdb: rdb}
    rdb.Del(ctx, "ratelimit:user:42")

    for i := 1; i <= 7; i++ {
        allowed, _ := rl.Allow(ctx, "user:42", 5, 10*time.Second)
        status := "✓ ALLOWED"
        if !allowed {
            status = "✗ BLOCKED"
        }
        fmt.Printf("  Request %d: %s\n", i, status)
    }

    // Cleanup
    rdb.Del(ctx, "demo:leaderboard")
}

Summary #

  • go-redis/v9 is the most complete Redis client for Go — supports Cluster, Sentinel, and pipelines.
  • redis.Nil isn’t a critical error — check with errors.Is(err, redis.Nil) for cache misses.
  • Hashes (HSet, HGetAll, HGetAll.Scan) for storing structs efficiently per field.
  • Sorted Sets for leaderboards, time-based rate limiting, and priority queues.
  • Pipelines for batched commands — reduce round trips, increase throughput.
  • WATCH + TxPipelined for optimistic transactions — retry on conflicts.
  • Distributed locks with SetNX and a Lua script for an atomically safe release.
  • Pub/Sub for lightweight real-time messaging — not a Kafka replacement for large-scale streaming.
  • Expire and TTL for cache lifetime management — always set a TTL to prevent memory exhaustion.
  • BLPOP for a reliable job queue — an efficient blocking pop without a polling loop.

← Previous: Google Pub/Sub   Next: Memcached →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact