Phastos Redis cache module provides a high-level wrapper around redigo with built-in connection pooling,
fallback pattern with singleflight deduplication, monitoring spans, and Redis Streams support.
Create a cache store with cache.New() using functional options:
import "github.com/kodekoding/phastos/v2/go/cache"
store := cache.New(
cache.WithAddress("localhost:6379"),
cache.WithDatabaseNo(0),
)| Option | Type | Default | Description |
|---|---|---|---|
WithAddress |
string |
"" |
Redis server address (host:port) |
WithDatabaseNo |
int |
0 |
Redis database number |
WithTimeout |
int |
0 |
Connection/read/write timeout in seconds |
WithMaxActive |
int |
MaxIdle * 5 |
Maximum active connections in pool |
WithMaxIdle |
int |
10 |
Maximum idle connections in pool |
WithMaxRetry |
int |
10 |
Max retries on ErrPoolExhausted (1s backoff) |
WithPassword |
string |
"" |
Redis password (AUTH) |
WithUsername |
string |
"" |
Redis username (ACL) |
All keys are automatically prefixed. The prefix is read from the REDIS_PREFIX_KEY environment variable,
defaulting to phastos:.
Uses redigo.Pool with:
TestOnBorrow: sendsPINGto validate connections before useMaxRetryonErrPoolExhaustedwith 1-second backoff between retries- On
New(), a PING is sent to verify connectivity — logs fatal if unreachable
err := store.Get(ctx, "user:1", &myStruct)- Auto JSON unmarshals into
typeDestination(must be a pointer) - If the destination is
*string, returns the raw string without JSON unmarshal - Returns
redigo.ErrNilfor cache miss (unless fallback is provided)
err := store.Set(ctx, "user:1", myStruct) // default 10 min TTL
err := store.Set(ctx, "user:1", myStruct, 3600) // 1 hour TTL
err := store.Set(ctx, "user:1", "string-value", 60) // raw string- Non-string values are auto JSON marshalled
- Default expiry: 10 minutes
- Uses Redis
SET ... EXcommand
deleted, err := store.Del(ctx, "user:1")- Returns count of deleted keys (
int64)
err := store.HGet(ctx, "user:1", "name", &name)
err := store.HGet(ctx, "user:1", "profile", &profile)- Gets a single field from a hash
- Auto JSON unmarshals into
typeDestination(must be a pointer) - If the destination is
*string, returns raw string - Supports fallback (see Fallback Pattern below)
err := store.HSet(ctx, "user:1", "name", "Raditya") // no expiry by default
err := store.HSet(ctx, "user:1", "profile", profile, 3600) // 1 hour TTL- Sets a single field in a hash
- Non-string values are auto JSON marshalled
- If
expireis provided and > 0, setsEXPIREon the key
err := store.HDel(ctx, "user:1", "name")- Deletes a single field from a hash
// Into a string map
var fields map[string]string
err := store.HGetAll(ctx, "user:1", &fields)
// Into a struct (JSON unmarshalled)
var profile UserProfile
err := store.HGetAll(ctx, "user:1", &profile)- Gets all fields of a hash
destmust be a pointer- If
destis*map[string]string, returns raw string map - Otherwise, marshals to JSON then unmarshals into
dest
err := store.HSetBulk(ctx, "user:1", map[string]interface{}{
"name": "Raditya",
"email": "raditya@example.com",
"role": "admin",
}, 3600) // 1 hour TTL- Sets multiple hash fields in one pipelined operation
- Non-string values are auto JSON marshalled
- Optional
expireparameter sets key TTL after all fields are written
When a cache miss occurs, the fallback function is called to fetch the data from the primary source
(e.g. database, API), auto-caches the result, and returns it. Concurrent requests for the same key
are deduplicated via singleflight.
type FallbackFn func(ctx context.Context) (result any, expire int64, err error)| Return | Description |
|---|---|
result |
The value to cache and return (any type — auto marshalled) |
expire |
Cache TTL in seconds (0 = defaults to 10 minutes) |
err |
Error from the primary source lookup |
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
var user User
err := store.Get(ctx, "user:1", &user, func(ctx context.Context) (any, int64, error) {
// Called only on cache miss
u, err := userRepo.FindByID(ctx, 1)
if err != nil {
return nil, 0, err
}
return u, 300, nil // cache for 5 minutes
})var profile Profile
err := store.HGet(ctx, "user:1", "profile", &profile, func(ctx context.Context) (any, int64, error) {
p, err := profileRepo.GetByUserID(ctx, 1)
if err != nil {
return nil, 0, err
}
return p, 600, nil // cache for 10 minutes
})If multiple concurrent requests miss the cache for the same key, only one fallback function is executed. All other callers wait for and receive the same result.
// 100 concurrent requests for "user:1"
// → only 1 DB query executes
// → result cached in Redis
// → all 100 callers receive the same valueThe singleflight key for Get is {prefix}{key}; for HGet it is {prefix}{key}:{field}.
Get/HGet → Redis miss → singleflight.Do(sfKey, ...)
→ fallbackFn(ctx) → fetch from DB/API
→ SET key value EX expire (or HSET + EXPIRE)
→ return cached value
messageID, err := store.PublishStream(ctx, "orders",
map[string]any{"action": "created", "order_id": "123"},
)- Accepts variadic
map[string]any(at least one required) - Uses
XADDwithMAXLEN ~ 100(approximate capping) - Returns the message ID
go store.SubscribeStream(ctx, "orders", func(ctx context.Context, data *cache.StreamData) error {
fmt.Println("Message ID:", data.ID)
fmt.Println("Fields:", data.Values)
// data.Values["action"] → "created"
// data.Values["order_id"] → "123"
return nil
})- Blocking call — run in a goroutine
- Uses
XREAD BLOCK 0(wait indefinitely for new messages) - Stops on context cancellation or after 10 consecutive failures
StreamDatacontainsID(message ID) andValues(field map)
// Create consumer group (with MKSTREAM — creates stream if not exists)
err := store.XGroupCreateMkStream(ctx, "orders", "order-processors", "0")
// Read pending/new messages as a consumer group member
msgs, err := store.XReadGroup(ctx, "order-processors", "worker-1",
[]string{"orders"}, []string{">"}, 5*time.Second, 10)
for _, stream := range msgs {
for _, msg := range stream.Messages {
// Process message
fmt.Println(msg.ID, msg.Values)
// Acknowledge
count, err := store.XAck(ctx, "orders", "order-processors", msg.ID)
_ = count
}
}XReadGroup: reads messages with consumer group semantics;">"for new messages,"0"for pendingXAck: acknowledges a message, returns count of acknowledged messagesblockparameter enables blocking read with timeoutcountlimits max messages returned
mux := http.NewServeMux()
mux.Handle("/api/", store.WrapToHandler(apiHandler))Stores the *Store in the request context. Use cache.GetCacheFromContext(ctx) in handlers.
ctx = store.WrapToContext(ctx)Stores the *Store in an existing context (useful outside HTTP handlers, e.g. background workers).
import "github.com/kodekoding/phastos/v2/go/cache"
func MyHandler(w http.ResponseWriter, r *http.Request) {
c := cache.GetCacheFromContext(r.Context())
if c == nil {
// cache not available in context
return
}
var user User
err := c.Get(r.Context(), "user:1", &user)
}- Returns
nilif no cache is found in the context - Returns the
Cachesinterface — use any cache operation
type Caches interface {
Get(ctx context.Context, key string, typeDestination any, fallbackFn ...FallbackFn) error
Del(ctx context.Context, key string) (int64, error)
Set(ctx context.Context, key string, value any, expire ...int) error
HSet(ctx context.Context, key, field string, value any, expire ...int) error
HGet(ctx context.Context, key, field string, typeDestination any, fallbackFn ...FallbackFn) error
HDel(ctx context.Context, key, field string) error
HGetAll(ctx context.Context, key string, dest interface{}) error
HSetBulk(ctx context.Context, key string, fields map[string]interface{}, expire ...int) error
XGroupCreateMkStream(ctx context.Context, streamKey, group, startID string) error
XReadGroup(ctx context.Context, group, consumer string, streams []string, ids []string, block time.Duration, count int64) ([]StreamMessages, error)
XAck(ctx context.Context, streamKey, group, id string) (int64, error)
}