cache

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 20, 2026 License: MIT Imports: 8 Imported by: 0

README

go-cache

CI Go Reference

A small, generic Go caching layer with a driver interface, TTL helpers, and a cache-aside Remember helper. Ships with in-memory and Redis drivers out of the box — write your own by implementing a four-method interface.

Why

Most projects end up hand-rolling the same "get from cache, else fetch and store" logic around every external call. go-cache factors that pattern out into one generic Remember method that works the same way regardless of the backing store, so swapping memory for Redis (or a custom driver) is a one-line change.

Install

go get github.com/PostScriptonLabs/go-cache

Requires Go 1.24+.

Architecture

  • Cache[Value] — the generic entry point, implements Cacher[Value] and owns backend-agnostic logic such as Remember.
  • Driver[Value] — the interface a storage backend implements: Get, Set, Has, Delete.
  • MemoryDriver[Value] — thread-safe in-process map with lazy expiry.
  • RedisDriver[Value] — Redis-backed driver with JSON encoding for non-string values, optional key prefixing, and TLS support.

Each driver has its own configuration options; Cache itself stays the same no matter which driver it wraps.

Quick start

In-memory cache
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/PostScriptonLabs/go-cache"
)

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

	c := cache.NewMemoryCache[string](
		cache.WithMemoryDefaultTTL(5 * time.Minute),
	)

	c.Set(ctx, "user:123", "John Doe", cache.DefaultTTL)
	c.Set(ctx, "otp:123", "1234", cache.WithTTL(30*time.Second))
	c.Set(ctx, "config:app", "production", cache.Forever)

	if value, found, err := c.Get(ctx, "user:123"); err == nil && found {
		fmt.Println("User:", value)
	}
}
Redis cache
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/PostScriptonLabs/go-cache"
)

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

	client, err := cache.NewRedisClient(
		ctx,
		cache.WithRedisAddr("localhost:6379"),
		cache.WithRedisDB(0),
	)
	if err != nil {
		fmt.Printf("connect to redis: %v\n", err)
		return
	}

	c := cache.NewRedisCache[string](client, cache.WithRedisDefaultTTL(5*time.Minute))

	c.Set(ctx, "user:123", "John Doe", cache.DefaultTTL)
	c.Set(ctx, "otp:123", "1234", cache.WithTTL(30*time.Second))
	c.Set(ctx, "config:app", "production", cache.Forever)

	if value, found, err := c.Get(ctx, "user:123"); err == nil && found {
		fmt.Println("User:", value)
	}
}

A runnable version of this lives in examples/redisREDIS_ADDR=localhost:6379 go run ./examples/redis.

Namespacing keys with a prefix

WithRedisPrefix prepends a fixed string to every key, which is handy when several applications share one Redis instance:

c := cache.NewRedisCache[string](
	client,
	cache.WithRedisPrefix("app:"), // "user:123" is stored as "app:user:123"
	cache.WithRedisDefaultTTL(5*time.Minute),
)
Authentication and TLS
client, err := cache.NewRedisClient(
	ctx,
	cache.WithRedisAddr("your-redis-host:6379"),
	cache.WithRedisUsername("default"),
	cache.WithRedisPassword("your-secure-password"),
	cache.WithRedisTLS(true),
	cache.WithRedisDB(0),
)

WithRedisTLS(true) skips certificate verification, which matches how managed Redis providers (e.g. AWS ElastiCache) commonly present self-signed certs. Pass false to disable TLS explicitly.

The Remember pattern

Remember is a cache-aside helper: look the key up, and only call fetch on a miss.

result, err := c.Remember(ctx, "expensive:calc", cache.DefaultTTL, func(ctx context.Context) (string, error) {
	// runs only when the key is absent from the cache
	return performExpensiveCalculation(ctx)
})

user, err := c.Remember(ctx, "api:user:123", cache.WithTTL(10*time.Minute), func(ctx context.Context) (User, error) {
	return fetchUserFromAPI(ctx, 123)
})

config, err := c.Remember(ctx, "app:config", cache.Forever, func(_ context.Context) (Config, error) {
	return loadConfigFromFile()
})

Behavior:

  1. Cache hit — the cached value is returned; fetch never runs.
  2. Cache missfetch runs, its result is stored with the given TTL, then returned.
  3. TTL expiry — the entry is dropped; the next call runs fetch again.
  4. Cache read error — treated like a miss, so a flaky cache backend degrades to "always fetch" instead of breaking the caller. If the subsequent Set also fails, Remember still returns the fetched value alongside a wrapped ErrCacheSetFailed, so callers can choose whether to treat it as fatal.

Sharing one Redis connection across several typed caches

A typical app needs more than one value type behind a cache (Cacher[User], Cacher[Config], Cacher[string], ...) while reusing a single Redis connection pool.

cmd/
  main.go
services/
  user_service.go    // uses Cacher[User]
  config_service.go  // uses Cacher[Config]
  auth_service.go    // uses Cacher[string]
// cmd/main.go
package main

import (
	"context"
	"fmt"
	"os"
	"time"

	"github.com/PostScriptonLabs/go-cache"
	"your-app/services"
)

type Caches struct {
	Users   cache.Cacher[User]
	Configs cache.Cacher[Config]
	Strings cache.Cacher[string]
}

type User struct {
	ID    int    `json:"id"`
	Name  string `json:"name"`
	Email string `json:"email"`
}

type Config struct {
	APIKey   string `json:"api_key"`
	Endpoint string `json:"endpoint"`
}

func newCaches(ctx context.Context) (*Caches, error) {
	client, err := cache.NewRedisClient(
		ctx,
		cache.WithRedisAddr(env("REDIS_ADDR", "localhost:6379")),
		cache.WithRedisPassword(env("REDIS_PASSWORD", "")),
		cache.WithRedisPoolSize(20),
	)
	if err != nil {
		return nil, fmt.Errorf("create redis client: %w", err)
	}

	opts := []cache.RedisDriverOption{
		cache.WithRedisDefaultTTL(5 * time.Minute),
		cache.WithRedisPrefix("app-name:"),
	}

	return &Caches{
		Users:   cache.NewRedisCache[User](client, opts...),
		Configs: cache.NewRedisCache[Config](client, opts...),
		Strings: cache.NewRedisCache[string](client, opts...),
	}, nil
}

func env(key, fallback string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return fallback
}

func main() {
	caches, err := newCaches(context.Background())
	if err != nil {
		fmt.Println(err)
		return
	}

	_ = services.NewUserService(caches.Users)
	_ = services.NewConfigService(caches.Configs)
	_ = services.NewAuthService(caches.Strings)
}
// services/user_service.go
package services

import (
	"context"
	"fmt"
	"time"

	"github.com/PostScriptonLabs/go-cache"
)

type UserService struct {
	cache cache.Cacher[User]
}

func NewUserService(c cache.Cacher[User]) *UserService {
	return &UserService{cache: c}
}

func (s *UserService) GetUser(ctx context.Context, id int) (User, error) {
	key := cache.Key(fmt.Sprintf("id:%d", id))

	return s.cache.Remember(ctx, key, cache.WithTTL(time.Hour), func(ctx context.Context) (User, error) {
		return s.fetchFromDB(ctx, id)
	})
}

Each driver instance is created per value type, but every driver reuses the same *redis.Client (and therefore the same connection pool), so the number of open Redis connections doesn't grow with the number of cached types.

Alternative: one Cacher[string] for everything

If you'd rather cap the connection pool at one cache instance, use a single Cacher[string] and marshal/unmarshal JSON yourself at the call site:

func GetUser(ctx context.Context, c cache.Cacher[string], id int) (User, error) {
	key := cache.Key(fmt.Sprintf("user:%d", id))

	raw, err := c.Remember(ctx, key, cache.WithTTL(time.Hour), func(ctx context.Context) (string, error) {
		user := User{ID: id, Name: "John Doe", Email: "john@example.com"}
		data, err := json.Marshal(user)
		return string(data), err
	})
	if err != nil {
		return User{}, err
	}

	var user User
	err = json.Unmarshal([]byte(raw), &user)
	return user, err
}

Trade-off: fewer connections, but no compile-time type safety and manual serialization for every type. Prefer typed caches (Cacher[User], ...) unless you're specifically constrained on connection pool size or need to cache many ad-hoc types.

TTL options

  • cache.DefaultTTL — use the default TTL configured on the cache/driver.
  • cache.WithTTL(d) — a fixed duration.
  • cache.WithTTLUntil(t) — a duration computed from now until t, useful when converting an absolute expiry timestamp.
  • cache.Forever — no expiration.
var (
	OTPTTL     = cache.WithTTL(30 * time.Second)
	SessionTTL = cache.WithTTL(30 * time.Minute)
	UserTTL    = cache.WithTTL(time.Hour)
)

c.Set(ctx, "otp:123", "1234", OTPTTL)
c.Set(ctx, "user:456", userData, UserTTL)

API reference

Cacher[Value]
type Cacher[Value any] interface {
	Get(ctx context.Context, key Key) (value Value, found bool, err error)
	Set(ctx context.Context, key Key, value Value, ttl TTL) error
	Has(ctx context.Context, key Key) (bool, error)
	Delete(ctx context.Context, key Key) error
	Remember(ctx context.Context, key Key, ttl TTL, fetch func(ctx context.Context) (Value, error)) (Value, error)
}
Constructors
  • NewMemoryCache[Value](options ...MemoryOption) *Cache[Value]
  • NewRedisCache[Value](client *redis.Client, options ...RedisDriverOption) *Cache[Value]
  • NewRedisClient(ctx context.Context, options ...RedisClientOption) (*redis.Client, error)
Memory options
  • WithMemoryDefaultTTL(d time.Duration)
Redis client options
  • WithRedisAddr(addr string)
  • WithRedisUsername(username string)
  • WithRedisPassword(password string)
  • WithRedisDB(db int)
  • WithRedisTLS(enable bool)
  • WithRedisPoolSize(size int)
  • WithRedisMinIdleConns(conns int)
  • WithRedisMaxRetries(retries int)
  • WithRedisTimeouts(dial, read, write, idle, maxConnAge time.Duration)
Redis driver options
  • WithRedisDefaultTTL(d time.Duration)
  • WithRedisPrefix(prefix string)

The RedisDriver also exposes Client(), Close(), Ping(ctx), FlushDB(ctx), Keys(ctx, pattern), TTL(ctx, key), and Exists(ctx, keys...) for cases where you need to drop down to Redis-specific operations.

Testing

go test ./...

The Redis driver's tests run against miniredis and don't require a live Redis server.

License

MIT

Documentation

Overview

Package cache provides a small, driver-based caching abstraction with pluggable in-memory and Redis backends, TTL helpers, and a cache-aside "Remember" helper for wrapping expensive or slow operations.

Index

Constants

This section is empty.

Variables

View Source
var (
	// DefaultTTL tells the driver to use whatever default TTL it was configured with.
	DefaultTTL = TTL{/* contains filtered or unexported fields */}
	// Forever stores an entry without expiration.
	Forever = TTL{/* contains filtered or unexported fields */}
)
View Source
var ErrCacheSetFailed = errors.New("cache set operation failed")

ErrCacheSetFailed is returned by Remember when the value was fetched successfully but could not be written back to the cache.

Functions

func NewRedisClient

func NewRedisClient(ctx context.Context, options ...RedisClientOption) (*redis.Client, error)

NewRedisClient creates a new Redis client from the provided options

Types

type Cache

type Cache[Value any] struct {
	// contains filtered or unexported fields
}

Cache is the main cache implementation that uses a driver

func NewCache

func NewCache[Value any](driver Driver[Value]) *Cache[Value]

NewCache creates a new cache instance with the specified driver

func NewMemoryCache

func NewMemoryCache[Value any](options ...MemoryOption) *Cache[Value]

NewMemoryCache creates a new cache instance with a memory driver This is a convenience function that combines NewMemoryDriver and NewCache

func NewRedisCache

func NewRedisCache[Value any](client *redis.Client, options ...RedisDriverOption) *Cache[Value]

NewRedisCache creates a new cache instance with a Redis driver This is a convenience function that combines NewRedisDriver and NewCache

func (*Cache[Value]) Delete

func (c *Cache[Value]) Delete(ctx context.Context, key Key) error

Delete removes a key from the cache

func (*Cache[Value]) Driver

func (c *Cache[Value]) Driver() Driver[Value]

Driver returns the underlying driver

func (*Cache[Value]) Get

func (c *Cache[Value]) Get(ctx context.Context, key Key) (Value, bool, error)

Get retrieves a value from the cache

func (*Cache[Value]) Has

func (c *Cache[Value]) Has(ctx context.Context, key Key) (bool, error)

Has checks if a key exists in the cache

func (*Cache[Value]) Remember

func (c *Cache[Value]) Remember(
	ctx context.Context,
	key Key,
	ttl TTL,
	fetch func(ctx context.Context) (Value, error),
) (Value, error)

Remember implements the cache-aside pattern This is common logic shared by all cache implementations

func (*Cache[Value]) Set

func (c *Cache[Value]) Set(ctx context.Context, key Key, value Value, ttl TTL) error

Set stores a value in the cache with the specified TTL

type Cacher

type Cacher[Value any] interface {
	Get(ctx context.Context, key Key) (value Value, found bool, err error)
	Set(ctx context.Context, key Key, value Value, ttl TTL) error
	Has(ctx context.Context, key Key) (bool, error)
	Delete(ctx context.Context, key Key) error

	Remember(ctx context.Context, key Key, ttl TTL, fetch func(ctx context.Context) (Value, error)) (Value, error)
}

Cacher is the main cache interface

type Driver

type Driver[Value any] interface {
	Get(ctx context.Context, key Key) (value Value, found bool, err error)
	Set(ctx context.Context, key Key, value Value, ttl TTL) error
	Has(ctx context.Context, key Key) (bool, error)
	Delete(ctx context.Context, key Key) error
}

Driver is the interface that cache drivers must implement Drivers handle the low-level storage operations

type Hit

type Hit[Value any] struct {
	Value  Value
	Expiry time.Time
}

type Key

type Key string

type MemoryConfig

type MemoryConfig struct {
	DefaultTTL time.Duration
}

MemoryConfig holds configuration for memory driver

type MemoryDriver

type MemoryDriver[Value any] struct {
	// contains filtered or unexported fields
}

MemoryDriver implements the Driver interface using in-memory storage

func NewMemoryDriver

func NewMemoryDriver[Value any](options ...MemoryOption) *MemoryDriver[Value]

NewMemoryDriver creates a new memory driver instance

func (*MemoryDriver[Value]) Delete

func (d *MemoryDriver[Value]) Delete(_ context.Context, key Key) error

Delete removes a key from memory storage

func (*MemoryDriver[Value]) Get

func (d *MemoryDriver[Value]) Get(_ context.Context, key Key) (Value, bool, error)

Get retrieves a value from memory storage

func (*MemoryDriver[Value]) Has

func (d *MemoryDriver[Value]) Has(ctx context.Context, key Key) (bool, error)

Has checks if a key exists in memory storage

func (*MemoryDriver[Value]) Set

func (d *MemoryDriver[Value]) Set(_ context.Context, key Key, value Value, ttl TTL) error

Set stores a value in memory storage with the specified TTL

type MemoryOption

type MemoryOption func(*MemoryConfig)

MemoryOption is a function that configures memory driver

func WithMemoryDefaultTTL

func WithMemoryDefaultTTL(duration time.Duration) MemoryOption

WithMemoryDefaultTTL sets the default TTL for memory driver

type RedisClientConfig

type RedisClientConfig struct {
	Addr         string        // Redis server address (e.g., "localhost:6379")
	Username     string        // Redis username (empty for no auth)
	Password     string        // Redis password (empty for no auth)
	DB           int           // Redis database number
	UseTLS       bool          // Enable TLS for Redis connection
	PoolSize     int           // Maximum number of socket connections
	MinIdleConns int           // Minimum number of idle connections
	MaxRetries   int           // Maximum number of retries
	DialTimeout  time.Duration // Dial timeout for establishing new connections
	ReadTimeout  time.Duration // Timeout for socket reads
	WriteTimeout time.Duration // Timeout for socket writes
	IdleTimeout  time.Duration // Close connections after remaining idle for this duration
	MaxConnAge   time.Duration // Close connections older than this duration
}

RedisClientConfig holds configuration for Redis client

type RedisClientOption

type RedisClientOption func(*RedisClientConfig)

RedisClientOption is a function that configures Redis client

func WithRedisAddr

func WithRedisAddr(addr string) RedisClientOption

WithRedisAddr sets the Redis server address

func WithRedisDB

func WithRedisDB(db int) RedisClientOption

WithRedisDB sets the Redis database number

func WithRedisMaxRetries

func WithRedisMaxRetries(retries int) RedisClientOption

WithRedisMaxRetries sets the maximum number of retries

func WithRedisMinIdleConns

func WithRedisMinIdleConns(conns int) RedisClientOption

WithRedisMinIdleConns sets the minimum number of idle connections

func WithRedisPassword

func WithRedisPassword(password string) RedisClientOption

WithRedisPassword sets the Redis password

func WithRedisPoolSize

func WithRedisPoolSize(size int) RedisClientOption

WithRedisPoolSize sets the maximum number of socket connections

func WithRedisTLS

func WithRedisTLS(enable bool) RedisClientOption

WithRedisTLS enables or disables TLS for Redis connection

func WithRedisTimeouts

func WithRedisTimeouts(dial, read, write, idle, maxConnAge time.Duration) RedisClientOption

WithRedisTimeouts sets various timeout configurations

func WithRedisUsername

func WithRedisUsername(username string) RedisClientOption

WithRedisUsername sets the Redis username

type RedisDriver

type RedisDriver[Value any] struct {
	// contains filtered or unexported fields
}

RedisDriver implements the Driver interface using Redis

func NewRedisDriver

func NewRedisDriver[Value any](client *redis.Client, options ...RedisDriverOption) *RedisDriver[Value]

NewRedisDriver creates a new Redis driver instance with the provided client

func (*RedisDriver[Value]) Client

func (d *RedisDriver[Value]) Client() *redis.Client

Client returns the underlying Redis client for advanced operations (pipelines, transactions, pub/sub, streams, custom commands).

Note that key prefixes configured via WithRedisPrefix option are not automatically applied when using the client directly.

func (*RedisDriver[Value]) Close

func (d *RedisDriver[Value]) Close() error

Close closes the Redis connection

func (*RedisDriver[Value]) Delete

func (d *RedisDriver[Value]) Delete(ctx context.Context, key Key) error

Delete removes a key from Redis

func (*RedisDriver[Value]) Exists

func (d *RedisDriver[Value]) Exists(ctx context.Context, keys ...Key) (int64, error)

Exists checks if multiple keys exist

func (*RedisDriver[Value]) FlushDB

func (d *RedisDriver[Value]) FlushDB(ctx context.Context) error

FlushDB flushes the current database (use with caution)

func (*RedisDriver[Value]) Get

func (d *RedisDriver[Value]) Get(ctx context.Context, key Key) (Value, bool, error)

Get retrieves a value from Redis

func (*RedisDriver[Value]) Has

func (d *RedisDriver[Value]) Has(ctx context.Context, key Key) (bool, error)

Has checks if a key exists in Redis

func (*RedisDriver[Value]) Keys

func (d *RedisDriver[Value]) Keys(ctx context.Context, pattern string) ([]string, error)

Keys returns all keys matching the pattern

func (*RedisDriver[Value]) Ping

func (d *RedisDriver[Value]) Ping(ctx context.Context) error

Ping tests the connection to Redis

func (*RedisDriver[Value]) Set

func (d *RedisDriver[Value]) Set(ctx context.Context, key Key, value Value, ttl TTL) error

Set stores a value in Redis with the specified TTL

func (*RedisDriver[Value]) TTL

func (d *RedisDriver[Value]) TTL(ctx context.Context, key Key) (time.Duration, error)

TTL returns the time to live for a key

type RedisDriverConfig

type RedisDriverConfig struct {
	DefaultTTL time.Duration // Default TTL for cache entries
	Prefix     string        // Prefix for all keys
}

RedisDriverConfig holds configuration for Redis driver

type RedisDriverOption

type RedisDriverOption func(*RedisDriverConfig)

RedisDriverOption is a function that configures Redis driver

func WithRedisDefaultTTL

func WithRedisDefaultTTL(duration time.Duration) RedisDriverOption

WithRedisDefaultTTL sets the default TTL for Redis driver

func WithRedisPrefix

func WithRedisPrefix(prefix string) RedisDriverOption

WithRedisPrefix sets the prefix for all keys in Redis driver

type TTL

type TTL struct {
	// contains filtered or unexported fields
}

TTL describes how long a cache entry should live. Build one with DefaultTTL, Forever, WithTTL, or WithTTLUntil.

func WithTTL

func WithTTL(d time.Duration) TTL

WithTTL builds a TTL with an explicit duration.

func WithTTLUntil

func WithTTLUntil(t time.Time) TTL

WithTTLUntil builds a TTL that expires at the given point in time.

It's handy when you already have an absolute expiry (e.g. an "expires_at" column from a database) and just need to convert it into a relative TTL.

Internally this is time.Until(t): if t is in the past the resulting duration is negative, and drivers that hand it straight to a backend such as Redis may expire the key immediately. Make sure t is in the future unless that's what you want.

Directories

Path Synopsis
examples
redis command
Command redis-example exercises the Redis driver end-to-end against a running Redis instance.
Command redis-example exercises the Redis driver end-to-end against a running Redis instance.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL