Redis is one of those tools that shows up in almost every production stack I've worked with. Caching, session storage, rate limiting, pub/sub. It does a lot of things well and it's fast. Pairing it with Go makes for a solid combination.

I covered session management with Redis in an earlier post, but here I want to go broader and cover the fundamentals of using Redis with Go.

Setting Up the Connection

I use the go-redis package. It's well maintained and has a clean API:

go get github.com/go-redis/redis/v8
package main

import (
    "context"
    "fmt"
    "github.com/go-redis/redis/v8"
)

var ctx = context.Background()

func main() {
    rdb := redis.NewClient(&redis.Options{
        Addr:     "localhost:6379",
        Password: "",
        DB:       0,
    })

    // Test the connection
    pong, err := rdb.Ping(ctx).Result()
    if err != nil {
        panic(err)
    }
    fmt.Println(pong) // PONG
}

Basic Operations

// Set a value with expiration
err := rdb.Set(ctx, "user:1:name", "Alice", 30*time.Minute).Err()

// Get a value
name, err := rdb.Get(ctx, "user:1:name").Result()
if err == redis.Nil {
    fmt.Println("Key does not exist")
} else if err != nil {
    panic(err)
}

// Delete a key
rdb.Del(ctx, "user:1:name")

// Check if key exists
exists, _ := rdb.Exists(ctx, "user:1:name").Result()

// Set expiration on existing key
rdb.Expire(ctx, "user:1:name", 1*time.Hour)

Caching Pattern

This is the pattern I use most often. Check Redis first, fall back to the database, then cache the result:

func GetUser(ctx context.Context, rdb *redis.Client, id string) (*User, error) {
    // Try cache first
    cached, err := rdb.Get(ctx, "user:"+id).Result()
    if err == nil {
        var user User
        json.Unmarshal([]byte(cached), &user)
        return &user, nil
    }

    // Cache miss, hit the database
    user, err := db.FindUser(id)
    if err != nil {
        return nil, err
    }

    // Cache for next time
    data, _ := json.Marshal(user)
    rdb.Set(ctx, "user:"+id, data, 15*time.Minute)

    return user, nil
}

Hash Maps

When you need to store structured data, Redis hashes are cleaner than serializing to JSON:

// Set hash fields
rdb.HSet(ctx, "user:1", map[string]interface{}{
    "name":  "Alice",
    "email": "alice@example.com",
    "role":  "admin",
})

// Get a single field
email, _ := rdb.HGet(ctx, "user:1", "email").Result()

// Get all fields
data, _ := rdb.HGetAll(ctx, "user:1").Result()
// data is map[string]string

Lists and Rate Limiting

A simple rate limiter using Redis sorted sets:

func IsRateLimited(ctx context.Context, rdb *redis.Client, userID string) bool {
    key := "ratelimit:" + userID
    now := time.Now().UnixNano()
    windowStart := now - int64(time.Minute)

    // Remove old entries
    rdb.ZRemRangeByScore(ctx, key, "0", fmt.Sprint(windowStart))

    // Count requests in window
    count, _ := rdb.ZCard(ctx, key).Result()
    if count >= 100 { // 100 requests per minute
        return true
    }

    // Add current request
    rdb.ZAdd(ctx, key, &redis.Z{Score: float64(now), Member: now})
    rdb.Expire(ctx, key, 2*time.Minute)

    return false
}

Connection Pooling

The go-redis client handles connection pooling automatically. You can tune it if needed:

rdb := redis.NewClient(&redis.Options{
    Addr:         "localhost:6379",
    PoolSize:     10,
    MinIdleConns: 5,
    PoolTimeout:  30 * time.Second,
})

For most applications, the defaults are fine. I only tune these when I'm seeing connection timeouts under load.

If you're setting up a full Go backend, also check out my posts on JSON marshaling, PostgreSQL transactions, and environment variables for the complete picture.