Redis Go
Introduction
Redis (Remote Dictionary Server) is an open-source, in-memory data structure store that can be used as a database, cache, message broker, and streaming engine. When combined with Go's performance and concurrency features, Redis becomes a powerful tool for building scalable applications.
This guide will walk you through integrating Redis with your Go applications. We'll cover how to connect to Redis servers, perform basic operations, implement common patterns, and explore real-world use cases.
Getting Started
Prerequisites
Before we begin, ensure you have:
- Go installed (version 1.15 or later recommended)
- Redis server installed and running locally or a Redis connection string
- Basic understanding of Go programming
Installation
Let's start by installing the most popular Redis client library for Go - go-redis
. Run the following command:
go get github.com/redis/go-redis/v9
Connecting to Redis
To connect to a Redis server from your Go application, you'll need to create a client:
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
func main() {
// Create a new context
ctx := context.Background()
// Create a new Redis client
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379", // Redis server address
Password: "", // Redis password, if any
DB: 0, // Redis database index
})
// Ping the Redis server to check connection
pong, err := rdb.Ping(ctx).Result()
if err != nil {
fmt.Printf("Failed to connect to Redis: %v
", err)
return
}
fmt.Printf("Connected to Redis: %s
", pong)
}
When you run this code, you should see:
Connected to Redis: PONG
This confirms your Go application can successfully connect to the Redis server.
Basic Redis Operations
Let's explore the fundamental operations you can perform with Redis in Go.
Storing and Retrieving Data
Redis supports various data types such as strings, lists, sets, and more. Here's how to work with string values:
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
"time"
)
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// Set a key with a string value
err := rdb.Set(ctx, "greeting", "Hello from Go!", 0).Err()
if err != nil {
fmt.Printf("Failed to set key: %v
", err)
return
}
// Get the value of a key
val, err := rdb.Get(ctx, "greeting").Result()
if err != nil {
fmt.Printf("Failed to get key: %v
", err)
return
}
fmt.Printf("greeting: %s
", val)
// Set with expiration (TTL)
err = rdb.Set(ctx, "temp_key", "This will expire", 5*time.Second).Err()
if err != nil {
fmt.Printf("Failed to set key with expiration: %v
", err)
return
}
// Check if key exists
time.Sleep(1 * time.Second)
ttl, err := rdb.TTL(ctx, "temp_key").Result()
if err != nil {
fmt.Printf("Failed to get TTL: %v
", err)
return
}
fmt.Printf("temp_key will expire in: %v
", ttl)
// Wait for key to expire
time.Sleep(5 * time.Second)
// Try to get expired key
val, err = rdb.Get(ctx, "temp_key").Result()
if err == redis.Nil {
fmt.Println("temp_key has expired")
} else if err != nil {
fmt.Printf("Error: %v
", err)
} else {
fmt.Printf("temp_key: %s
", val)
}
}
Output:
greeting: Hello from Go!
temp_key will expire in: 3.999s
temp_key has expired
Working with Hash Maps
Redis hashes are maps between string fields and string values, making them perfect for representing objects:
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// Create a user profile using a hash
_, err := rdb.HSet(ctx, "user:100", map[string]interface{}{
"username": "gopher",
"email": "[email protected]",
"visits": 1,
}).Result()
if err != nil {
fmt.Printf("Failed to set hash: %v
", err)
return
}
// Increment the visits counter
visits, err := rdb.HIncrBy(ctx, "user:100", "visits", 1).Result()
if err != nil {
fmt.Printf("Failed to increment counter: %v
", err)
return
}
fmt.Printf("User visit count: %d
", visits)
// Get a specific field
username, err := rdb.HGet(ctx, "user:100", "username").Result()
if err != nil {
fmt.Printf("Failed to get field: %v
", err)
return
}
fmt.Printf("Username: %s
", username)
// Get all fields
userData, err := rdb.HGetAll(ctx, "user:100").Result()
if err != nil {
fmt.Printf("Failed to get all fields: %v
", err)
return
}
fmt.Println("User profile:")
for field, value := range userData {
fmt.Printf(" %s: %s
", field, value)
}
}
Output:
User visit count: 2
Username: gopher
User profile:
email: [email protected]
username: gopher
visits: 2
Lists Operations
Redis lists are linked lists of string values, ideal for implementing queues:
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// Pushing elements to a list
_, err := rdb.LPush(ctx, "tasks", "Task 1", "Task 2", "Task 3").Result()
if err != nil {
fmt.Printf("Failed to push to list: %v
", err)
return
}
// Get list length
length, err := rdb.LLen(ctx, "tasks").Result()
if err != nil {
fmt.Printf("Failed to get list length: %v
", err)
return
}
fmt.Printf("Number of tasks: %d
", length)
// Get all elements in the list
tasks, err := rdb.LRange(ctx, "tasks", 0, -1).Result()
if err != nil {
fmt.Printf("Failed to get list range: %v
", err)
return
}
fmt.Println("All tasks:")
for i, task := range tasks {
fmt.Printf(" %d: %s
", i+1, task)
}
// Pop elements from the list (simulating a queue)
task, err := rdb.RPop(ctx, "tasks").Result()
if err != nil {
fmt.Printf("Failed to pop from list: %v
", err)
return
}
fmt.Printf("Processing task: %s
", task)
// Get updated list
tasks, err = rdb.LRange(ctx, "tasks", 0, -1).Result()
if err != nil {
fmt.Printf("Failed to get updated list: %v
", err)
return
}
fmt.Println("Remaining tasks:")
for i, task := range tasks {
fmt.Printf(" %d: %s
", i+1, task)
}
}
Output:
Number of tasks: 3
All tasks:
1: Task 3
2: Task 2
3: Task 1
Processing task: Task 1
Remaining tasks:
1: Task 3
2: Task 2
Common Redis Patterns in Go
Let's explore some common patterns and use cases for Redis in Go applications.
Caching
One of the most common uses for Redis is as a caching layer. Here's a simple caching implementation:
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/redis/go-redis/v9"
"time"
)
// Product represents a product in our system
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
}
// Simulated database lookup - in reality, this would query a database
func getProductFromDB(id int) (*Product, error) {
// Imagine this is a slow database query
time.Sleep(200 * time.Millisecond)
// Return dummy product
return &Product{
ID: id,
Name: fmt.Sprintf("Product %d", id),
Price: 19.99 + float64(id),
}, nil
}
// Get product with caching
func getProduct(ctx context.Context, rdb *redis.Client, id int) (*Product, error) {
cacheKey := fmt.Sprintf("product:%d", id)
// Try to get from cache first
cachedData, err := rdb.Get(ctx, cacheKey).Result()
if err == nil {
// Cache hit!
var product Product
if err := json.Unmarshal([]byte(cachedData), &product); err != nil {
return nil, fmt.Errorf("failed to unmarshal cached product: %v", err)
}
fmt.Println("Cache hit! Returning cached product")
return &product, nil
} else if err != redis.Nil {
// Some other error occurred
return nil, fmt.Errorf("cache error: %v", err)
}
// Cache miss, get from database
fmt.Println("Cache miss! Fetching from database...")
product, err := getProductFromDB(id)
if err != nil {
return nil, fmt.Errorf("database error: %v", err)
}
// Store in cache for future requests (expires in 1 hour)
productJSON, err := json.Marshal(product)
if err != nil {
return nil, fmt.Errorf("failed to marshal product: %v", err)
}
err = rdb.Set(ctx, cacheKey, productJSON, time.Hour).Err()
if err != nil {
return nil, fmt.Errorf("failed to store in cache: %v", err)
}
return product, nil
}
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// First request will be a cache miss
product, err := getProduct(ctx, rdb, 42)
if err != nil {
fmt.Printf("Error: %v
", err)
return
}
fmt.Printf("Product: %+v
", product)
// Second request should be a cache hit
product, err = getProduct(ctx, rdb, 42)
if err != nil {
fmt.Printf("Error: %v
", err)
return
}
fmt.Printf("Product: %+v
", product)
}
Output:
Cache miss! Fetching from database...
Product: &{ID:42 Name:Product 42 Price:61.99}
Cache hit! Returning cached product
Product: &{ID:42 Name:Product 42 Price:61.99}
Rate Limiting
Redis is excellent for implementing rate limiting. Here's a simple implementation:
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
"time"
)
// RateLimiter implements a simple rate limiter using Redis
type RateLimiter struct {
rdb *redis.Client
requestsLimit int
timeWindow time.Duration
}
// NewRateLimiter creates a new rate limiter
func NewRateLimiter(rdb *redis.Client, requestsLimit int, timeWindow time.Duration) *RateLimiter {
return &RateLimiter{
rdb: rdb,
requestsLimit: requestsLimit,
timeWindow: timeWindow,
}
}
// Allow checks if a request is allowed for a given key
func (rl *RateLimiter) Allow(ctx context.Context, key string) (bool, int, error) {
// Create a Redis key specific to this user/IP and current time window
redisKey := fmt.Sprintf("rate_limit:%s:%d", key, time.Now().Unix()/int64(rl.timeWindow.Seconds()))
// Increment counter for the current window
count, err := rl.rdb.Incr(ctx, redisKey).Result()
if err != nil {
return false, 0, fmt.Errorf("rate limit error: %v", err)
}
// Set expiration (if it doesn't exist)
if count == 1 {
err = rl.rdb.Expire(ctx, redisKey, rl.timeWindow).Err()
if err != nil {
return false, int(count), fmt.Errorf("rate limit expiration error: %v", err)
}
}
// Check if we're over the limit
remaining := rl.requestsLimit - int(count)
if remaining < 0 {
remaining = 0
}
return count <= int64(rl.requestsLimit), remaining, nil
}
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// Create a rate limiter: 3 requests per 10 seconds
limiter := NewRateLimiter(rdb, 3, 10*time.Second)
// Simulate requests from user "john"
for i := 1; i <= 5; i++ {
allowed, remaining, err := limiter.Allow(ctx, "john")
if err != nil {
fmt.Printf("Error: %v
", err)
return
}
if allowed {
fmt.Printf("Request %d allowed, remaining: %d
", i, remaining)
} else {
fmt.Printf("Request %d blocked, try again later (exceeded limit of 3 requests per 10 seconds)
", i)
}
// Simulate short delay between requests
time.Sleep(500 * time.Millisecond)
}
}
Output:
Request 1 allowed, remaining: 2
Request 2 allowed, remaining: 1
Request 3 allowed, remaining: 0
Request 4 blocked, try again later (exceeded limit of 3 requests per 10 seconds)
Request 5 blocked, try again later (exceeded limit of 3 requests per 10 seconds)
Distributed Locks
Redis can be used to implement distributed locks for coordinating access to shared resources:
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
"time"
)
// DistributedLock represents a distributed lock using Redis
type DistributedLock struct {
rdb *redis.Client
key string
value string
duration time.Duration
}
// NewDistributedLock creates a new distributed lock
func NewDistributedLock(rdb *redis.Client, key, value string, duration time.Duration) *DistributedLock {
return &DistributedLock{
rdb: rdb,
key: fmt.Sprintf("lock:%s", key),
value: value,
duration: duration,
}
}
// Acquire attempts to acquire the lock
func (dl *DistributedLock) Acquire(ctx context.Context) (bool, error) {
// Try to set the key only if it doesn't exist (NX) with an expiration (EX)
// This is an atomic operation in Redis
success, err := dl.rdb.SetNX(ctx, dl.key, dl.value, dl.duration).Result()
if err != nil {
return false, fmt.Errorf("lock acquisition error: %v", err)
}
return success, nil
}
// Release releases the lock if it belongs to us
func (dl *DistributedLock) Release(ctx context.Context) (bool, error) {
// Create a Lua script for atomic release
// This script ensures we only delete the key if it has the expected value
script := `
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
`
// Execute the script
result, err := dl.rdb.Eval(ctx, script, []string{dl.key}, dl.value).Result()
if err != nil {
return false, fmt.Errorf("lock release error: %v", err)
}
// If result is 1, the key was deleted (we owned the lock)
return result.(int64) == 1, nil
}
func processSharedResource(ctx context.Context, rdb *redis.Client, worker string) {
// Create a lock with a 10-second expiration
lock := NewDistributedLock(rdb, "shared_resource", worker, 10*time.Second)
// Try to acquire the lock
acquired, err := lock.Acquire(ctx)
if err != nil {
fmt.Printf("Worker %s error: %v
", worker, err)
return
}
if !acquired {
fmt.Printf("Worker %s couldn't acquire lock, resource is busy
", worker)
return
}
fmt.Printf("Worker %s acquired lock, processing shared resource...
", worker)
// Simulate work
time.Sleep(2 * time.Second)
fmt.Printf("Worker %s finished processing
", worker)
// Release the lock
released, err := lock.Release(ctx)
if err != nil {
fmt.Printf("Worker %s error releasing lock: %v
", worker, err)
return
}
if released {
fmt.Printf("Worker %s released lock
", worker)
} else {
fmt.Printf("Worker %s couldn't release lock (it may have expired)
", worker)
}
}
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// Run the first worker
processSharedResource(ctx, rdb, "Worker-A")
// Run a second worker that will try to acquire the same lock immediately
go processSharedResource(ctx, rdb, "Worker-B")
// Wait a bit and try another worker
time.Sleep(1 * time.Second)
go processSharedResource(ctx, rdb, "Worker-C")
// Let everything finish
time.Sleep(5 * time.Second)
}
Output:
Worker-A acquired lock, processing shared resource...
Worker-B couldn't acquire lock, resource is busy
Worker-A finished processing
Worker-A released lock
Worker-C acquired lock, processing shared resource...
Worker-C finished processing
Worker-C released lock
Real-World Application: Activity Feed
Let's build a simple activity feed using Redis sorted sets to track and display user activities:
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/redis/go-redis/v9"
"time"
)
// Activity represents a user activity
type Activity struct {
UserID int `json:"user_id"`
Action string `json:"action"`
Target string `json:"target"`
Timestamp time.Time `json:"timestamp"`
}
// ActivityFeed manages the activity feed using Redis
type ActivityFeed struct {
rdb *redis.Client
}
// NewActivityFeed creates a new activity feed manager
func NewActivityFeed(rdb *redis.Client) *ActivityFeed {
return &ActivityFeed{rdb: rdb}
}
// AddActivity adds a new activity to the feed
func (af *ActivityFeed) AddActivity(ctx context.Context, activity Activity) error {
// Set timestamp if not set
if activity.Timestamp.IsZero() {
activity.Timestamp = time.Now()
}
// Convert activity to JSON
activityJSON, err := json.Marshal(activity)
if err != nil {
return fmt.Errorf("failed to marshal activity: %v", err)
}
// Add to the global feed
globalKey := "feeds:global"
_, err = af.rdb.ZAdd(ctx, globalKey, redis.Z{
Score: float64(activity.Timestamp.Unix()),
Member: activityJSON,
}).Result()
if err != nil {
return fmt.Errorf("failed to add to global feed: %v", err)
}
// Trim the global feed to last 1000 items
_, err = af.rdb.ZRemRangeByRank(ctx, globalKey, 0, -1001).Result()
if err != nil {
return fmt.Errorf("failed to trim global feed: %v", err)
}
// Add to the user's feed
userKey := fmt.Sprintf("feeds:user:%d", activity.UserID)
_, err = af.rdb.ZAdd(ctx, userKey, redis.Z{
Score: float64(activity.Timestamp.Unix()),
Member: activityJSON,
}).Result()
if err != nil {
return fmt.Errorf("failed to add to user feed: %v", err)
}
// Trim the user feed to last 100 items
_, err = af.rdb.ZRemRangeByRank(ctx, userKey, 0, -101).Result()
if err != nil {
return fmt.Errorf("failed to trim user feed: %v", err)
}
return nil
}
// GetGlobalFeed gets the global activity feed
func (af *ActivityFeed) GetGlobalFeed(ctx context.Context, count int) ([]Activity, error) {
// Get activities from global feed, most recent first
results, err := af.rdb.ZRevRange(ctx, "feeds:global", 0, int64(count-1)).Result()
if err != nil {
return nil, fmt.Errorf("failed to get global feed: %v", err)
}
return af.parseActivities(results)
}
// GetUserFeed gets a specific user's activity feed
func (af *ActivityFeed) GetUserFeed(ctx context.Context, userID, count int) ([]Activity, error) {
// Get activities from user feed, most recent first
results, err := af.rdb.ZRevRange(ctx, fmt.Sprintf("feeds:user:%d", userID), 0, int64(count-1)).Result()
if err != nil {
return nil, fmt.Errorf("failed to get user feed: %v", err)
}
return af.parseActivities(results)
}
// parseActivities parses JSON activities
func (af *ActivityFeed) parseActivities(results []string) ([]Activity, error) {
activities := make([]Activity, 0, len(results))
for _, result := range results {
var activity Activity
if err := json.Unmarshal([]byte(result), &activity); err != nil {
return nil, fmt.Errorf("failed to unmarshal activity: %v", err)
}
activities = append(activities, activity)
}
return activities, nil
}
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
feed := NewActivityFeed(rdb)
// Add some activities
activities := []Activity{
{UserID: 101, Action: "posted", Target: "article:1234", Timestamp: time.Now().Add(-time.Hour)},
{UserID: 102, Action: "commented", Target: "article:1234", Timestamp: time.Now().Add(-30 * time.Minute)},
{UserID: 101, Action: "liked", Target: "comment:5678", Timestamp: time.Now().Add(-20 * time.Minute)},
{UserID: 103, Action: "shared", Target: "article:1234", Timestamp: time.Now().Add(-10 * time.Minute)},
{UserID: 101, Action: "replied", Target: "comment:9012", Timestamp: time.Now().Add(-5 * time.Minute)},
}
for _, activity := range activities {
if err := feed.AddActivity(ctx, activity); err != nil {
fmt.Printf("Error adding activity: %v
", err)
return
}
}
// Get global feed
globalActivities, err := feed.GetGlobalFeed(ctx, 10)
if err != nil {
fmt.Printf("Error getting global feed: %v
", err)
return
}
fmt.Println("Global Activity Feed:")
for i, activity := range globalActivities {
fmt.Printf("%d. User %d %s %s at %s
",
i+1, activity.UserID, activity.Action, activity.Target,
activity.Timestamp.Format(time.RFC3339))
}
fmt.Println()
// Get user feed
userActivities, err := feed.GetUserFeed(ctx, 101, 10)
if err != nil {
fmt.Printf("Error getting user feed: %v
", err)
return
}
fmt.Println("User 101 Activity Feed:")
for i, activity := range userActivities {
fmt.Printf("%d. User %d %s %s at %s
",
i+1, activity.UserID, activity.Action, activity.Target,
activity.Timestamp.Format(time.RFC3339))
}
}
Output:
Global Activity Feed:
1. User 101 replied comment:9012 at 2023-05-25T14:55:00Z
2. User 103 shared article:1234 at 2023-05-25T14:50:00Z
3. User 101 liked comment:5678 at 2023-05-25T14:40:00Z
4. User 102 commented article:1234 at 2023-05-25T14:30:00Z
5. User 101 posted article:1234 at 2023-05-25T14:00:00Z
User 101 Activity Feed:
1. User 101 replied comment:9012 at 2023-05-25T14:55:00Z
2. User 101 liked comment:5678 at 2023-05-25T14:40:00Z
3. User 101 posted article:1234 at 2023-05-25T14:00:00Z
Redis Connection Pooling
For production applications, it's essential to manage Redis connections efficiently:
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
"time"
)
func main() {
// Create a Redis client with connection pooling options
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // Redis password
DB: 0, // Redis database index
PoolSize: 10, // Maximum number of socket connections
MinIdleConns: 5, // Minimum number of idle connections
DialTimeout: 5 * time.Second, // Dial timeout for establishing new connections
ReadTimeout: 3 * time.Second, // Read timeout for socket reads
WriteTimeout: 3 * time.Second, // Write timeout for socket writes
PoolTimeout: 4 * time.Second, // Timeout for getting a connection from the pool
})
ctx := context.Background()
// Check connection
_, err := rdb.Ping(ctx).Result()
if err != nil {
panic(fmt.Sprintf("Failed to connect to Redis: %v", err))
}
// Display pool stats
stats := rdb.PoolStats()
fmt.Printf("Redis connection pool stats:
")
fmt.Printf(" Total connections: %d
", stats.TotalConns)
fmt.Printf(" Idle connections: %d
", stats.IdleConns)
fmt.Printf(" Stale connections: %d
", stats.StaleConns)
// Close connections when done
defer rdb.Close()
}
Redis Pub/Sub in Go
Redis Pub/Sub provides a simple messaging system:
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
"time"
)
func startSubscriber(ctx context.Context, rdb *redis.Client, channel string) {
pubsub := rdb.Subscribe(ctx, channel)
defer pubsub.Close()
// Wait for confirmation that subscription is created
_, err := pubsub.Receive(ctx)
if err != nil {
panic(fmt.Sprintf("Error setting up subscription: %v", err))
}
// Start receiving messages
ch := pubsub.Channel()
fmt.Printf("Subscriber: Listening for messages on channel '%s'...
", channel)
for msg := range ch {
fmt.Printf("Subscriber: Received message: %s
", msg.Payload)
// For demo purposes, exit after a few messages
if msg.Payload == "message 3" {
fmt.Println("Subscriber: Closing subscription after receiving 3 messages")
return
}
}
}
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// Define channel name
channel := "notifications"
// Start subscriber in a goroutine
go startSubscriber(ctx, rdb, channel)
// Give the subscriber time to start
time.Sleep(1 * time.Second)
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)