gostore

package module
v1.2.3 Latest Latest
Warning

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

Go to latest
Published: Nov 13, 2025 License: MIT Imports: 10 Imported by: 0

README

codecov Test status Go Report Card GoDev GitHub release License

gostore

gostore is a simple yet powerful key-value store written in Go that provides a fast access read interface through intelligent caching. Built on top of BBolt (a port of BoltDB to Go), it offers persistent storage with an optional LRU cache layer for high-performance read operations.

Features

  • Persistent Storage: Built on BBolt for reliable, ACID-compliant data persistence
  • High-Performance Caching: Optional LRU cache with TTL support for fast reads
  • Namespace Support: Organize data using buckets/namespaces
  • TTL Support: Automatic expiration of cached and stored data
  • Memoization: Cache function results with singleflight protection to prevent cache stampede
  • Type Safety: Full support for Go types implementing encoding.BinaryMarshaler
  • Robust Error Handling: Comprehensive input validation and clear error messages
  • Configurable: Flexible configuration options for retries, cache size, and read-only mode

Installation

go get github.com/millken/gostore

Quick Start

Basic Usage
package main

import (
    "encoding/json"
    "fmt"
    "log"

    "github.com/millken/gostore"
)

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func (u User) MarshalBinary() ([]byte, error) {
    return json.Marshal(u)
}

func (u *User) UnmarshalBinary(data []byte) error {
    return json.Unmarshal(data, u)
}

func main() {
    // Open a store with LRU cache
    store, err := gostore.Open("/tmp/mydb",
        gostore.WithMaxCacheSize(1000),
        gostore.WithNumRetries(3),
    )
    if err != nil {
        log.Fatal(err)
    }
    defer store.Close()

    // Store a user
    user := User{Name: "Alice", Age: 30}
    err = store.Update("user:1", &user)
    if err != nil {
        log.Fatal(err)
    }

    // Load the user
    var loadedUser User
    err = store.Load("user:1", &loadedUser)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Loaded user: %+v\n", loadedUser)
}
In-Memory Store (for testing)
package main

import (
    "fmt"
    "log"

    "github.com/millken/gostore"
)

func main() {
    // Create an in-memory store for testing
    store, err := gostore.OpenMemory(
        gostore.WithMaxCacheSize(1000),
    )
    if err != nil {
        log.Fatal(err)
    }
    defer store.Close() // Automatically cleans up temp files

    // Use it exactly like a regular store
    err = store.Put("test", []byte("key"), []byte("value"))
    if err != nil {
        log.Fatal(err)
    }

    value, err := store.Get([]byte("test"), []byte("key"))
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Value: %s\n", string(value))
}

API Reference

Opening a Store
// Basic file-based store
store, err := gostore.Open("/path/to/db")

// With options
store, err := gostore.Open("/path/to/db",
    gostore.WithMaxCacheSize(1000),     // LRU cache size
    gostore.WithNumRetries(5),          // Retry attempts for DB operations
    gostore.WithReadOnly(),             // Read-only mode
)

// In-memory store for testing
memoryStore, err := gostore.OpenMemory(
    gostore.WithMaxCacheSize(1000),     // LRU cache size
    gostore.WithNumRetries(3),          // Retry attempts
)
Configuration Options
  • WithMaxCacheSize(size int): Set maximum number of items in LRU cache (0 = no cache)
  • WithNumRetries(n uint8): Set number of retry attempts (1-10, default: 3)
  • WithReadOnly(): Open store in read-only mode
Basic Operations
Put/Get Operations
// Store raw bytes
err := store.Put("namespace", []byte("key"), []byte("value"))

// Store with TTL (seconds)
err := store.PutWithTTL([]byte("namespace"), []byte("key"), []byte("value"), 3600)

// Retrieve raw bytes
value, err := store.Get([]byte("namespace"), []byte("key"))

// Delete a key
err := store.Delete("namespace", []byte("key"))

// Delete entire namespace
err := store.DeleteNamespace("namespace")
Type-Safe Operations
// Store any type implementing encoding.BinaryMarshaler
err := store.Update("user:1", &User{Name: "Bob"})

// Store with TTL
err := store.UpdateWithTTL("user:2", &User{Name: "Charlie"}, 3600)

// Load into any type implementing encoding.BinaryUnmarshaler
var user User
err := store.Load("user:1", &user)

// Remove a key
err := store.Remove("user:1")
Memoization

Cache expensive function calls with automatic singleflight protection:

var result Result
err := store.Memoize("expensive_operation", &result, func() (interface{}, error) {
    // This function will only be called once per key
    return performExpensiveOperation()
})

// With TTL
err := store.MemoizeWithTTL("cached_result", &result, func() (interface{}, error) {
    return fetchDataFromAPI()
}, 300) // 5 minutes TTL

Error Handling

gostore provides clear, typed errors for different scenarios:

var (
    gostore.ErrKeyNotFound     // Key doesn't exist
    gostore.ErrKeyExpired      // Key has expired
    gostore.ErrBadValue        // Invalid value (doesn't implement BinaryMarshaler)
    gostore.ErrInvalidInput    // Invalid input parameters
)

Example:

import (
    "errors"
    "fmt"
    "log"
)

err := store.Load("nonexistent", &value)
if errors.Is(err, gostore.ErrKeyNotFound) {
    fmt.Println("Key does not exist")
} else if err != nil {
    log.Printf("Unexpected error: %v", err)
}

Performance

gostore is optimized for high-performance scenarios:

  • Cached reads: ~400ns per operation
  • Regular reads: ~900ns per operation
  • Writes: ~3.7μs per operation

Benchmarks show excellent performance with minimal memory allocations thanks to the efficient LRU cache implementation and BBolt's optimized storage engine.

Use Cases

  • Configuration storage: Store and cache application configuration
  • Session management: Fast user session storage with automatic expiration
  • Caching layer: Memoize expensive function calls and API responses
  • Metadata storage: Store file metadata, indexes, or auxiliary data
  • Local caching: Fast local cache for distributed applications

Thread Safety

gostore is thread-safe for concurrent read operations. Write operations are serialized through BBolt's transaction system. The built-in singleflight mechanism ensures safe memoization across multiple goroutines.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

Development

# Run tests
go test -v ./...

# Run benchmarks
go test -bench=. -benchmem

# Run tests with race detector
go test -race -v ./...

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrKeyNotFound is returned when the key supplied to a Get or Delete
	// method does not exist in the database.
	ErrKeyNotFound = errors.New("key not found")

	// ErrKeyExpired is returned when the key supplied to a Get or Delete
	ErrKeyExpired = errors.New("key expired")

	// ErrBadValue is returned when the value supplied to the Put method
	// is nil or when a value doesn't implement the required interface.
	ErrBadValue = errors.New("bad value: value must implement encoding.BinaryMarshaler")

	// ErrInvalidInput is returned when input parameters are invalid
	ErrInvalidInput = errors.New("invalid input parameters")
)

Functions

This section is empty.

Types

type Option

type Option func(*option) error

Option the tracer provider option

func WithMaxCacheSize

func WithMaxCacheSize(maxCacheSize int) Option

WithMaxCacheSize sets the maximum number of items in the LRU cache.

func WithNumRetries

func WithNumRetries(n uint8) Option

WithNumRetries defines the number of retry attempts for database operations

func WithReadOnly

func WithReadOnly() Option

WithReadOnly set the store to read-only mode

type Store

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

Store is KVStore implementation based bolt DB

func Open

func Open(DbPath string, opts ...Option) (*Store, error)

Open opens a store with the given config

func OpenMemory added in v1.2.2

func OpenMemory(opts ...Option) (*Store, error)

OpenMemory opens a store in memory for testing purposes

func (*Store) Close

func (s *Store) Close() error

Close closes the store

func (*Store) Delete

func (s *Store) Delete(namespace string, key []byte) error

Delete deletes a record by key

func (*Store) DeleteNamespace added in v1.1.0

func (s *Store) DeleteNamespace(namespace string) error

DeleteNamespace deletes a namespace

func (*Store) Get

func (s *Store) Get(namespace, key []byte) ([]byte, error)

Get fetches a value by key

func (*Store) Load

func (s *Store) Load(key string, obj encoding.BinaryUnmarshaler) error

Load read value by key

func (*Store) Memoize

func (s *Store) Memoize(key string, obj encoding.BinaryUnmarshaler, f func() (any, error)) error

Memoize memoize a function

func (*Store) MemoizeWithTTL added in v1.1.0

func (s *Store) MemoizeWithTTL(key string, obj encoding.BinaryUnmarshaler, f func() (any, error), ttl int64) error

func (*Store) Put

func (s *Store) Put(namespace string, key, value []byte) (err error)

Put inserts a <key, value> record

func (*Store) PutWithTTL added in v1.1.0

func (s *Store) PutWithTTL(namespace, key, value []byte, ttl int64) (err error)

PutWithTTL inserts a <key, value> record with TTL

func (*Store) Remove

func (s *Store) Remove(key string) error

Remove delete a record by key

func (*Store) Update

func (s *Store) Update(key string, value encoding.BinaryMarshaler) error

Update set value by key, value must be implement encoding.BinaryMarshaler

func (*Store) UpdateWithTTL added in v1.1.0

func (s *Store) UpdateWithTTL(key string, value encoding.BinaryMarshaler, ttl int64) error

UpdateWithTTL set value by key with TTL, value must be implement encoding.BinaryMarshaler

Jump to

Keyboard shortcuts

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