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 Structure | Short Description | Main go-redis Methods | Example Use Cases |
|---|---|---|---|
| String | A single text/numeric value | Get, Set, SetNX | JSON caching, session tokens, distributed locks |
| Hash | A structured key-value map | HGet, HSet, HGetAll | Storing object data (e.g. user profiles) |
| List | An ordered sequence of strings | LPush, RPop, BLPop | Task queues, log lists |
| Set | An unordered collection of unique strings | SAdd, SMembers, SIsMember | Duplicate filtering, friends/followers relations |
| Sorted Set | A unique set ordered by score | ZAdd, ZRangeByScore | Building 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 --> ReturnComplete 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/v9is the most complete Redis client for Go — supports Cluster, Sentinel, and pipelines.redis.Nilisn’t a critical error — check witherrors.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+TxPipelinedfor optimistic transactions — retry on conflicts.- Distributed locks with
SetNXand a Lua script for an atomically safe release.- Pub/Sub for lightweight real-time messaging — not a Kafka replacement for large-scale streaming.
ExpireandTTLfor cache lifetime management — always set a TTL to prevent memory exhaustion.BLPOPfor a reliable job queue — an efficient blocking pop without a polling loop.