Go Cache Fetcherクライアント

よくあるRedisやmemcacheでキャッシュするパターンをサポートするライブラリのGoクライアントを作りました。
こんなやつです。

```
アクセスしたい情報や変数からキー名を生成する
if (Redisにデータがある) {
  RedisからGetする
} else {
  DatabaseからGetしたり、APIからfetchする
  RedisへSetする
}
```

Redisで使う想定ですが、Clientを実装すればどのようなキャッシュにも使えます。
functionを渡せばそれがキャッシュされるので、DBでもAPIアクセスでも使えます。
データをserializeして set, getすることもできます。

https://github.com/peutes/go-cache-fetcher-client

詳しい使い方は、READMEとテストコードを見てください。

# go-cache-fetcher-client

Golangのキャッシュフェッチャーです。

## Installation

```
go get github.com/peutes/cachefetcher
```

## ✅ キャッシュフェッチャークライアントの基本ロジックをサポート

様々なファンクションのレスポンスをRedis, Memcached、その他のキャッシュシステムにキャッシュできます。
たとえば、最初にファンクションからレスポンスを取得する際にRedisに保存し、ファンクションのレスポンスを取得し、
次にもしキャッシュがあったらRedisから取得できます。


## ✅ 要素からキャッシュキーを自動生成

キャッシュキーの生成ロジックについて考える必要はありません。
キー生成ロジックはメインロジックから隠蔽できます。


## ✅ シンプルなキャッシュコントロール

キャッシュコントロールはとてもシンプルで、キーをセットし、フェッチャー関数をフェッチするだけです。

このフェッチャークライアントは使用するためには、 `SetKey` と `Fetch` 関数のみで十分に使用できます。
`Fetch` には、フェッチャー関数、取得用値のポインタ、キャッシュ有効期限をセットします。

- `SetKey()`
- `Fetch()`

## インストール 

```
go get github.com/peutes/go-cache-fetcher-client
```

## 使い方

```go
fetcher := cachefetcher.NewCacheFetcher(
  &ClientImpl{
    Rdb: redis.NewClient(&redis.Options{Addr: "localhost:6379"}),
  },
  nil
)
fetcher.SetKey([]string{"prefix", "str"}, "hoge")
// fetcher.Key() == "prefix_str_hoge"

// フェッチャー関数。たとえば、DBから読み込みを想定します。
read := func(s string) string {
  return s + " fetch!!"
}

// 最初のフェッチはフェッチャー関数から呼ばれます。キャッシュは呼ばれません。
var dst string
err := fetcher.Fetch(10*time.Second, &dst, func() (string, error) {
  return read("first"), nil
})
// dst == "first fetch!!" <- get from function

// ２回目のフェチは有効期限内のキャッシュから呼ばれます。フェッチャー関数からは呼ばれません。
err = fetcher.Fetch(10*time.Second, &dst, func() (string, error) {
  return read("second"), nil
})
// dst == "first fetch!!" <- get from cache

```

### サポートされてる型

キー要素は、stringの他、 int, float, bool, complex, byte, time, slice, array, `String()` メソッドがある struct に対応しています。
このクライアントは gob serializer によるシリアライズをサポートしています。
キャッシュにはシリアライズされた文字列が保存されます。

```go
fetcher.SetKey([]string{"prefix", "any"}, 1, 0.1, true, &[]string{"a", "b"}, time.Unix(0, 0).In(time.UTC))
_ = fetcher.Key() // "prefix_any_1_0.1_true_a_b_1970-01-01_00:00:00_+0000_UTC"
_ = fetcher.HashKey() // "prefix_any_c94a415eb6e20585f4fbc856b6edcf52007259522967c4bea548515e71531663"

read := func() ([]int, error) {
  return []int{1, 2, 3, 4, 5}
}
var dst []int  
err := fetcher.Fetch(10*time.Second, &dst, read)
// dst == []int{1, 2, 3, 4, 5}

```

要素は、文字列タイプ以外もサポートしています。
もし interface{} や独自のタイプを使用したい場合は、　`GobRegister()` を使って登録します。

```go
    i := 10
    b := true
    s := "abc"
    ft := 0.123
    i8 := int8(20)
    i64 := int64(30)
    ui8 := uint8(40)
    ui64 := uint64(50)

    e := &testStruct{
        I:     i,
        B:     b,
        S:     s,
        F:     ft,
        I8:    i8,
        I64:   i64,
        UI8:   ui8,
        UI64:  ui64,
        IP:    &i,
        BP:    &b,
        SP:    &s,
        FP:    &ft,
        I8P:   &i8,
        I64P:  &i64,
        UI8P:  &ui8,
        UI64P: &ui64,
        IS:    []int{i, i, i},
        BS:    []bool{b, b, b},
        SS:    []string{s, s, s},
        FS:    []float64{ft, ft, ft},
        IM:    map[int]int{1: i, 2: i, 3: i},
        BM:    map[bool]bool{true: b, false: b},
        SM:    map[string]string{"a": s, "bb": s, "ccc": s},
        FM:    map[float64]float64{0.1: ft, 0.2: ft, 0.3: ft},
    }

    var dst testStruct
    f := cachefetcher.NewCacheFetcher(redisClient, options)
    err := fetcher.SetKey([]string{"prefix", "key"}, "struct1")
    err = fetcher.Set(e, 10*time.Second)
    err = fetcher.Get(&dst)
```

### 他のキャッシュコントロール

もし、ハッシュキーが必要であれば、 `SetKey` のかわりに、 `SetHashKey` を使います。

個々に、 `Set()`, `Get()`, `Del()` も使えます。
もし、キーが欲しい場合は、`Key()`を使います。
もし、キャッシュされているか？の結果をboolで欲しい場合は、`IsCached()`を使います。

- `SetHashKey()`
- `Set()`
- `Get()`
- `SetString()`
- `GetString()`
- `Del()`
- `Key()`
- `IsCached()`
- `GobRegister()`
- 
### キャッシュクライアントの実装

このキャッシュフェッチャークライアントは、キャッシュクライアントの実装が求められますが、
独自実装する代わりに必要最低限のシンプルなRedisクライアントを用意しているので、このクライアントを使えば動かすことができます。

https://github.com/peutes/go-cache-fetcher/blob/main/cachefetcher/simple_redis_client.go

```go
// SimpleRedisClientImpl はサンプルクライアントの実装です。
type SimpleRedisClientImpl struct {
    Rdb *redis.Client
}

// サンプルクライアントの `Set` 実装です。
func (i *SimpleRedisClientImpl) Set(key string, value interface{}, expiration time.Duration) error {
    // You need an implementation to set from the cache.
    return i.Rdb.Set(ctx, key, value, expiration).Err()
}

// サンプルクライアントの `Get` 実装です。
func (i *SimpleRedisClientImpl) Get(key string, dst interface{}) error {
    // You need an implementation to get from the cache.
    v, err := i.Rdb.Get(ctx, key).Result()
    if err != nil {
        return err
    }

    reflect.ValueOf(dst).Elem().SetString(v)
    return nil
}

// サンプルクライアントの `Del` 実装です。
func (i *SimpleRedisClientImpl) Del(key string) error {
    return i.Rdb.Del(ctx, key).Err()
}

// サンプルクライアントの `IsErrCacheMiss` 実装です。
// キャッシュミスエラーの判定を返してください。
func (i *SimpleRedisClientImpl) IsErrCacheMiss(err error) bool {
    return errors.Is(err, redis.Nil)
}

```

### オプション

このフェッチャークライアントは single flight を設定で使えます。

もし、 `DebugPrintMode` を有効にすると、ターミナルにキーが出力されます。

```go
cachefetcher.Options{
    Group:          &singleflight.Group{}, // default
    GroupTimeout:   30 * time.Second,      // default
    DebugPrintMode: true,                  // default is false
})
```