Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

## Features
- Memoizes functions with TTL, supporting 0 to 7 comparable parameters. [List of Memoize Functions](https://github.com/agkloop/go_memoize/blob/main/memoize.go)
- Error-aware variants (suffix `E`) that support compute functions returning `(V, error)` and do NOT cache when an error is returned.
- High performance, zero allocation, and zero dependencies.
- Utilizes the FNV-1a hash algorithm for caching.
- Thread-safe and concurrent-safe.
Expand Down Expand Up @@ -44,6 +45,36 @@ memoizedCtxFn := MemoizeCtx(computeCtxFn, 10*time.Second)
result := memoizedCtxFn(context.Background())
```

### Error-aware memoization (do not memoize errors)

If your compute function can fail and returns `(V, error)`, use the `E` variants. These versions will NOT store a cached value when the compute function returns a non-nil error. This is useful for transient failures where you want the next call to retry the computation rather than returning a cached error result.

Available `E` variants:
- `MemoizeE` (no-arg)
- `Memoize1E` .. `Memoize7E` (1..7 args)
- `MemoizeCtxE` and `MemoizeCtx1E` .. `MemoizeCtx7E` (context-aware)

Behavior:
- If a cached value exists for the key, the function returns it and a nil error.
- If no cached value exists, the compute function is executed.
- If compute returns `(v, nil)`, `v` is cached and returned.
- If compute returns `(zeroValue, err)` (err != nil), the error is returned and nothing is cached.

Example:

```go
computeFn := func(id int) (string, error) {
// may return an error sometimes
}

memo := Memoize1E(func(id int) (string, error) { return computeFn(id) }, 30*time.Second)

val, err := memo(123)
if err != nil {
// transient error; next call will retry since nothing was cached
}
```

### Memoization with Parameters

The package provides functions to memoize functions with up to 7 parameters. Here are some examples:
Expand Down Expand Up @@ -124,6 +155,20 @@ result := memoizedCtxFn(context.Background(), 5, "example", 3.14)

The `Cache` struct is used internally to manage the cached entries. It supports setting, getting, and deleting entries, as well as computing new values if they are not already cached or have expired.

## Testing

Unit tests cover the memoization behavior, including the new error-aware variants. To run tests:

```sh
# run all tests
go test ./...

# run a specific test
go test ./... -run TestMemoizeE_DoesNotCacheError -v
```

New tests were added in `memoize_error_test.go` to verify that error results are not cached and that successful results are cached.

## Example

Here is a complete example of using the `memoize` package:
Expand Down Expand Up @@ -244,6 +289,46 @@ result := memoizedFn(1, 2, 3, 4, 5, 6, 7)
</code></pre>
</td>
</tr>
<tr>
<td><code>MemoizeE</code></td>
<td>Memoizes a function with no params, error-aware</td>
<td>
<pre><code>
memoizedFn := MemoizeE(func() (int, error) { return 1, nil }, time.Minute)
result, err := memoizedFn()
</code></pre>
</td>
</tr>
<tr>
<td><code>Memoize1E</code></td>
<td>Memoizes a function with 1 param, error-aware</td>
<td>
<pre><code>
memoizedFn := Memoize1E(func(a int) (int, error) { return a * 2, nil }, time.Minute)
result, err := memoizedFn(5)
</code></pre>
</td>
</tr>
<tr>
<td><code>Memoize2E</code></td>
<td>Memoizes a function with 2 params, error-aware</td>
<td>
<pre><code>
memoizedFn := Memoize2E(func(a int, b string) (string, error) { return fmt.Sprintf("%d-%s", a, b), nil }, time.Minute)
result, err := memoizedFn(5, "example")
</code></pre>
</td>
</tr>
<tr>
<td><code>Memoize3E</code></td>
<td>Memoizes a function with 3 params, error-aware</td>
<td>
<pre><code>
memoizedFn := Memoize3E(func(a int, b string, c float64) (string, error) { return fmt.Sprintf("%d-%s-%f", a, b, c), nil }, time.Minute)
result, err := memoizedFn(5, "example", 3.14)
</code></pre>
</td>
</tr>
<tr>
<td><code>MemoizeCtx</code></td>
<td>Memoizes a function with context and no params</td>
Expand Down
139 changes: 139 additions & 0 deletions memoize.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,142 @@ func Memoize7[K1, K2, K3, K4, K5, K6, K7 comparable, V any](computeFn func(K1, K
})
}
}

// --- New variants that return an error and avoid caching when computeFn returns a non-nil error ---

// MemoizeE memoizes a function that returns (V, error). Errors are not cached.
func MemoizeE[V any](computeFn func() (V, error), ttl time.Duration) func() (V, error) {
cache := NewCacheSized[uint64, V](1, int64(ttl.Seconds()))
return func() (V, error) {
// try cached
if v, ok := cache.Get(0); ok {
return v, nil
}
// compute
v, err := computeFn()
if err != nil {
return zeroValue[V](), err
}
cache.Set(0, v)
return v, nil
}
}

// Memoize1E memoizes a function with 1 arg that returns (V, error). Errors are not cached.
func Memoize1E[K comparable, V any](computeFn func(K) (V, error), ttl time.Duration) func(K) (V, error) {
cache := NewCache[uint64, V](int64(ttl.Seconds()))
return func(k K) (V, error) {
key := hash1(k)
if v, ok := cache.Get(key); ok {
return v, nil
}
v, err := computeFn(k)
if err != nil {
return zeroValue[V](), err
}
cache.Set(key, v)
return v, nil
}
}

// Memoize2E memoizes a function with 2 args that returns (V, error). Errors are not cached.
func Memoize2E[K1, K2 comparable, V any](computeFn func(K1, K2) (V, error), ttl time.Duration) func(K1, K2) (V, error) {
cache := NewCache[uint64, V](int64(ttl.Seconds()))
return func(key1 K1, key2 K2) (V, error) {
key := hash2(key1, key2)
if v, ok := cache.Get(key); ok {
return v, nil
}
v, err := computeFn(key1, key2)
if err != nil {
return zeroValue[V](), err
}
cache.Set(key, v)
return v, nil
}
}

// Memoize3E memoizes a function with 3 args that returns (V, error). Errors are not cached.
func Memoize3E[K1, K2, K3 comparable, V any](computeFn func(K1, K2, K3) (V, error), ttl time.Duration) func(K1, K2, K3) (V, error) {
cache := NewCache[uint64, V](int64(ttl.Seconds()))
return func(key1 K1, key2 K2, key3 K3) (V, error) {
key := hash3(key1, key2, key3)
if v, ok := cache.Get(key); ok {
return v, nil
}
v, err := computeFn(key1, key2, key3)
if err != nil {
return zeroValue[V](), err
}
cache.Set(key, v)
return v, nil
}
}

// Memoize4E memoizes a function with 4 args that returns (V, error). Errors are not cached.
func Memoize4E[K1, K2, K3, K4 comparable, V any](computeFn func(K1, K2, K3, K4) (V, error), ttl time.Duration) func(K1, K2, K3, K4) (V, error) {
cache := NewCache[uint64, V](int64(ttl.Seconds()))
return func(key1 K1, key2 K2, key3 K3, key4 K4) (V, error) {
key := hash4(key1, key2, key3, key4)
if v, ok := cache.Get(key); ok {
return v, nil
}
v, err := computeFn(key1, key2, key3, key4)
if err != nil {
return zeroValue[V](), err
}
cache.Set(key, v)
return v, nil
}
}

// Memoize5E memoizes a function with 5 args that returns (V, error). Errors are not cached.
func Memoize5E[K1, K2, K3, K4, K5 comparable, V any](computeFn func(K1, K2, K3, K4, K5) (V, error), ttl time.Duration) func(K1, K2, K3, K4, K5) (V, error) {
cache := NewCache[uint64, V](int64(ttl.Seconds()))
return func(key1 K1, key2 K2, key3 K3, key4 K4, key5 K5) (V, error) {
key := hash5(key1, key2, key3, key4, key5)
if v, ok := cache.Get(key); ok {
return v, nil
}
v, err := computeFn(key1, key2, key3, key4, key5)
if err != nil {
return zeroValue[V](), err
}
cache.Set(key, v)
return v, nil
}
}

// Memoize6E memoizes a function with 6 args that returns (V, error). Errors are not cached.
func Memoize6E[K1, K2, K3, K4, K5, K6 comparable, V any](computeFn func(K1, K2, K3, K4, K5, K6) (V, error), ttl time.Duration) func(K1, K2, K3, K4, K5, K6) (V, error) {
cache := NewCache[uint64, V](int64(ttl.Seconds()))
return func(key1 K1, key2 K2, key3 K3, key4 K4, key5 K5, key6 K6) (V, error) {
key := hash6(key1, key2, key3, key4, key5, key6)
if v, ok := cache.Get(key); ok {
return v, nil
}
v, err := computeFn(key1, key2, key3, key4, key5, key6)
if err != nil {
return zeroValue[V](), err
}
cache.Set(key, v)
return v, nil
}
}

// Memoize7E memoizes a function with 7 args that returns (V, error). Errors are not cached.
func Memoize7E[K1, K2, K3, K4, K5, K6, K7 comparable, V any](computeFn func(K1, K2, K3, K4, K5, K6, K7) (V, error), ttl time.Duration) func(K1, K2, K3, K4, K5, K6, K7) (V, error) {
cache := NewCache[uint64, V](int64(ttl.Seconds()))
return func(key1 K1, key2 K2, key3 K3, key4 K4, key5 K5, key6 K6, key7 K7) (V, error) {
key := hash7(key1, key2, key3, key4, key5, key6, key7)
if v, ok := cache.Get(key); ok {
return v, nil
}
v, err := computeFn(key1, key2, key3, key4, key5, key6, key7)
if err != nil {
return zeroValue[V](), err
}
cache.Set(key, v)
return v, nil
}
}
137 changes: 137 additions & 0 deletions memoize_ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,140 @@ func MemoizeCtx7[K1, K2, K3, K4, K5, K6, K7 comparable, V any](computeFn func(co
})
}
}

// --- New context-aware variants returning (V, error) that avoid caching errors ---

// MemoizeCtxE memoizes a context-aware function returning (V, error). Errors are not cached.
func MemoizeCtxE[V any](computeFn func(context.Context) (V, error), ttl time.Duration) func(context.Context) (V, error) {
cache := NewCacheSized[uint64, V](1, int64(ttl.Seconds()))
return func(ctx context.Context) (V, error) {
if v, ok := cache.Get(0); ok {
return v, nil
}
v, err := computeFn(ctx)
if err != nil {
return zeroValue[V](), err
}
cache.Set(0, v)
return v, nil
}
}

// MemoizeCtx1E memoizes a context-aware function with 1 arg returning (V, error). Errors are not cached.
func MemoizeCtx1E[K comparable, V any](computeFn func(context.Context, K) (V, error), ttl time.Duration) func(context.Context, K) (V, error) {
cache := NewCache[uint64, V](int64(ttl.Seconds()))
return func(ctx context.Context, k K) (V, error) {
key := hash1(k)
if v, ok := cache.Get(key); ok {
return v, nil
}
v, err := computeFn(ctx, k)
if err != nil {
return zeroValue[V](), err
}
cache.Set(key, v)
return v, nil
}
}

// MemoizeCtx2E memoizes a context-aware function with 2 args returning (V, error). Errors are not cached.
func MemoizeCtx2E[K1, K2 comparable, V any](computeFn func(context.Context, K1, K2) (V, error), ttl time.Duration) func(context.Context, K1, K2) (V, error) {
cache := NewCache[uint64, V](int64(ttl.Seconds()))
return func(ctx context.Context, key1 K1, key2 K2) (V, error) {
key := hash2(key1, key2)
if v, ok := cache.Get(key); ok {
return v, nil
}
v, err := computeFn(ctx, key1, key2)
if err != nil {
return zeroValue[V](), err
}
cache.Set(key, v)
return v, nil
}
}

// MemoizeCtx3E memoizes a context-aware function with 3 args returning (V, error). Errors are not cached.
func MemoizeCtx3E[K1, K2, K3 comparable, V any](computeFn func(context.Context, K1, K2, K3) (V, error), ttl time.Duration) func(context.Context, K1, K2, K3) (V, error) {
cache := NewCache[uint64, V](int64(ttl.Seconds()))
return func(ctx context.Context, key1 K1, key2 K2, key3 K3) (V, error) {
key := hash3(key1, key2, key3)
if v, ok := cache.Get(key); ok {
return v, nil
}
v, err := computeFn(ctx, key1, key2, key3)
if err != nil {
return zeroValue[V](), err
}
cache.Set(key, v)
return v, nil
}
}

// MemoizeCtx4E memoizes a context-aware function with 4 args returning (V, error). Errors are not cached.
func MemoizeCtx4E[K1, K2, K3, K4 comparable, V any](computeFn func(context.Context, K1, K2, K3, K4) (V, error), ttl time.Duration) func(context.Context, K1, K2, K3, K4) (V, error) {
cache := NewCache[uint64, V](int64(ttl.Seconds()))
return func(ctx context.Context, key1 K1, key2 K2, key3 K3, key4 K4) (V, error) {
key := hash4(key1, key2, key3, key4)
if v, ok := cache.Get(key); ok {
return v, nil
}
v, err := computeFn(ctx, key1, key2, key3, key4)
if err != nil {
return zeroValue[V](), err
}
cache.Set(key, v)
return v, nil
}
}

// MemoizeCtx5E memoizes a context-aware function with 5 args returning (V, error). Errors are not cached.
func MemoizeCtx5E[K1, K2, K3, K4, K5 comparable, V any](computeFn func(context.Context, K1, K2, K3, K4, K5) (V, error), ttl time.Duration) func(context.Context, K1, K2, K3, K4, K5) (V, error) {
cache := NewCache[uint64, V](int64(ttl.Seconds()))
return func(ctx context.Context, key1 K1, key2 K2, key3 K3, key4 K4, key5 K5) (V, error) {
key := hash5(key1, key2, key3, key4, key5)
if v, ok := cache.Get(key); ok {
return v, nil
}
v, err := computeFn(ctx, key1, key2, key3, key4, key5)
if err != nil {
return zeroValue[V](), err
}
cache.Set(key, v)
return v, nil
}
}

// MemoizeCtx6E memoizes a context-aware function with 6 args returning (V, error). Errors are not cached.
func MemoizeCtx6E[K1, K2, K3, K4, K5, K6 comparable, V any](computeFn func(context.Context, K1, K2, K3, K4, K5, K6) (V, error), ttl time.Duration) func(context.Context, K1, K2, K3, K4, K5, K6) (V, error) {
cache := NewCache[uint64, V](int64(ttl.Seconds()))
return func(ctx context.Context, key1 K1, key2 K2, key3 K3, key4 K4, key5 K5, key6 K6) (V, error) {
key := hash6(key1, key2, key3, key4, key5, key6)
if v, ok := cache.Get(key); ok {
return v, nil
}
v, err := computeFn(ctx, key1, key2, key3, key4, key5, key6)
if err != nil {
return zeroValue[V](), err
}
cache.Set(key, v)
return v, nil
}
}

// MemoizeCtx7E memoizes a context-aware function with 7 args returning (V, error). Errors are not cached.
func MemoizeCtx7E[K1, K2, K3, K4, K5, K6, K7 comparable, V any](computeFn func(context.Context, K1, K2, K3, K4, K5, K6, K7) (V, error), ttl time.Duration) func(context.Context, K1, K2, K3, K4, K5, K6, K7) (V, error) {
cache := NewCache[uint64, V](int64(ttl.Seconds()))
return func(ctx context.Context, key1 K1, key2 K2, key3 K3, key4 K4, key5 K5, key6 K6, key7 K7) (V, error) {
key := hash7(key1, key2, key3, key4, key5, key6, key7)
if v, ok := cache.Get(key); ok {
return v, nil
}
v, err := computeFn(ctx, key1, key2, key3, key4, key5, key6, key7)
if err != nil {
return zeroValue[V](), err
}
cache.Set(key, v)
return v, nil
}
}
Loading
Loading