Keyspaces
Typed, namespaced key-value partitions with automatic prefix management, per-keyspace codecs, default TTLs, and type-safe access.
A Keyspace[T] is a typed, namespaced partition of a KV store. It binds a Go type, key prefix, codec, and TTL policy together so that every operation within the keyspace is type-safe, consistently prefixed, and automatically serialized.
Why Keyspaces?
Without keyspaces, you manage key prefixes and type assertions manually:
// Manual approach -- error-prone and repetitive.
store.Set(ctx, "users:u_123", &user, kv.WithTTL(24*time.Hour))
var user User
store.Get(ctx, "users:u_123", &user)With keyspaces, the prefix, TTL, and codec are declared once:
// Keyspace approach -- type-safe and DRY.
users := keyspace.New[User](store, "users",
keyspace.WithTTL(24*time.Hour),
)
users.Set(ctx, "u_123", user)
user, err := users.Get(ctx, "u_123")The keyspace automatically prepends "users:" to every key, applies a 24-hour TTL to every Set, and returns a typed User from every Get -- no type assertions needed.
Creating a Keyspace
import (
"github.com/xraph/grove/kv/keyspace"
"github.com/xraph/grove/kv/codec"
)
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
users := keyspace.New[User](store, "users")The type parameter T determines the value type for all operations. The prefix string is prepended to every key with a separator (default ":").
Options
| Option | Description |
|---|---|
keyspace.WithTTL(d) | Default TTL for all Set operations in the keyspace |
keyspace.WithCodec(c) | Override the store-level codec for this keyspace |
keyspace.WithSeparator(sep) | Change the key segment separator (default ":") |
sessions := keyspace.New[Session](store, "session",
keyspace.WithTTL(30*time.Minute),
keyspace.WithCodec(codec.MsgPack()),
keyspace.WithSeparator("/"),
)
// Keys will be "session/abc123" instead of "session:abc123".If no codec is specified, the keyspace inherits the store's default codec.
Methods
Get
Retrieves and decodes the value for the given key. Returns a typed T value directly.
func (ks *Keyspace[T]) Get(ctx context.Context, key string) (T, error)user, err := users.Get(ctx, "u_123")
if errors.Is(err, kv.ErrNotFound) {
// key does not exist
}
fmt.Println(user.Name)GetEntry
Retrieves the value with full metadata including TTL, version, and key.
func (ks *Keyspace[T]) GetEntry(ctx context.Context, key string) (*kv.Entry[T], error)entry, err := users.GetEntry(ctx, "u_123")
fmt.Println(entry.Value.Name) // the User
fmt.Println(entry.Key) // "users:u_123"
fmt.Println(entry.TTL) // remaining TTL
fmt.Println(entry.Version) // CAS version (if supported)The Entry[T] struct:
type Entry[T any] struct {
Key string
Value T
TTL time.Duration
Version uint64
Metadata map[string]string
}Set
Encodes and stores the value. Applies the keyspace's default TTL unless overridden by SetOption values.
func (ks *Keyspace[T]) Set(ctx context.Context, key string, value T, opts ...kv.SetOption) errorerr := users.Set(ctx, "u_123", User{
ID: "u_123",
Name: "Alice",
Email: "alice@example.com",
})Override the default TTL for a single call:
err := users.Set(ctx, "u_temp", tempUser, kv.WithTTL(5*time.Minute))Delete
Removes one or more keys within the keyspace.
func (ks *Keyspace[T]) Delete(ctx context.Context, keys ...string) errorerr := users.Delete(ctx, "u_123", "u_456")Exists
Returns the count of keys that exist within the keyspace.
func (ks *Keyspace[T]) Exists(ctx context.Context, keys ...string) (int64, error)count, err := users.Exists(ctx, "u_123", "u_456")MGet
Retrieves multiple keys within the keyspace. Returns a typed map[string]T keyed by the original (unprefixed) key names.
func (ks *Keyspace[T]) MGet(ctx context.Context, keys []string) (map[string]T, error)result, err := users.MGet(ctx, []string{"u_123", "u_456"})
for key, user := range result {
fmt.Printf("%s => %s\n", key, user.Name)
}Scan
Iterates over keys matching a pattern suffix within the keyspace. The keys passed to the callback are stripped of the keyspace prefix.
func (ks *Keyspace[T]) Scan(ctx context.Context, pattern string, fn func(key string) error) errorerr := users.Scan(ctx, "*", func(key string) error {
fmt.Println("user key:", key) // prints "u_123", not "users:u_123"
return nil
})Prefix and Store
Access the keyspace's prefix and underlying store:
fmt.Println(users.Prefix()) // "users"
store := users.Store() // the underlying *kv.StoreKey Composition
The keyspace package includes helpers for building and parsing composite keys:
ComposeKey
Joins segments with a separator:
key := keyspace.ComposeKey(":", "org", "team", "member")
// "org:team:member"ParseKey
Splits a composite key into segments:
segments := keyspace.ParseKey("org:team:member", ":")
// ["org", "team", "member"]Join
Shorthand for ComposeKey with ":" as the separator:
key := keyspace.Join("org", "team", "member")
// "org:team:member"Complete Example
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/xraph/grove/kv"
"github.com/xraph/grove/kv/codec"
"github.com/xraph/grove/kv/keyspace"
"github.com/xraph/grove/kv/drivers/redisdriver"
)
type User struct {
ID string `json:"id" msgpack:"id"`
Name string `json:"name" msgpack:"name"`
Email string `json:"email" msgpack:"email"`
Role string `json:"role" msgpack:"role"`
}
func main() {
ctx := context.Background()
rdb := redisdriver.New()
if err := rdb.Open(ctx, "redis://localhost:6379/0"); err != nil {
log.Fatal(err)
}
store, err := kv.Open(rdb)
if err != nil {
log.Fatal(err)
}
defer store.Close()
// Create a typed keyspace with a 24-hour default TTL and MsgPack codec.
users := keyspace.New[User](store, "users",
keyspace.WithTTL(24*time.Hour),
keyspace.WithCodec(codec.MsgPack()),
)
// Set.
alice := User{ID: "u_1", Name: "Alice", Email: "alice@example.com", Role: "admin"}
if err := users.Set(ctx, alice.ID, alice); err != nil {
log.Fatal(err)
}
// Get -- returns a typed User directly.
user, err := users.Get(ctx, "u_1")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Name: %s, Role: %s\n", user.Name, user.Role)
// GetEntry -- includes metadata.
entry, err := users.GetEntry(ctx, "u_1")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Key: %s, TTL: %s\n", entry.Key, entry.TTL)
// Scan all user keys.
err = users.Scan(ctx, "*", func(key string) error {
fmt.Println("found user:", key)
return nil
})
if err != nil {
log.Fatal(err)
}
}