Go (Gin Web)でサーバーキャッシュを扱う

# Go (Gin Web)でサーバーキャッシュを扱う
Goでサーバーキャッシュを扱うときの方法を簡単にまとめます。
大規模のトラフィックを捌くときにお役に立てばと思います。
フレームワークはGin Webを利用する想定です。

## main.goでキャッシュの接続を記述
```go:main.go
package main

import (
	"os"
	"time"
	"tkol-lite/controllers"
	"tkol-lite/models"
	"tkol-lite/services"

	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
)

func main() {

	os.Setenv("TZ", "UTC") // タイムゾーンの設定
	r := gin.Default()

	r.Use(cors.New(cors.Config{

		AllowOrigins: []string{ // アクセスを許可したいアクセス元
			"http://example.com",
		},

		AllowMethods: []string{ // アクセスを許可したいHTTPメソッド
			"POST",
			"GET",
			"PUT",
			"DELETE",
		},

		AllowHeaders: []string{ // 許可したいHTTPリクエストヘッダ
			"Access-Control-Allow-Credentials",
			"Access-Control-Allow-Headers",
			"Content-Type",
			"Content-Length",
			"Accept-Encoding",
			"Authorization",
		},

		AllowCredentials: true, // cookieなどの情報を必要とするかどうか

		MaxAge: 24 * time.Hour, // preflightリクエストの結果をキャッシュする時間
	}))

	models.ConnectDatabase() // DB初期化
	models.CreateCache()     // キャッシュ初期化
    
    // （中略）
    r.GET("/article/:id", controllers.FetchArticleById)
    // （中略）
    
	r.Run()
}

```

## Controllerからキャッシュの読み込み、書き込み
キャッシュに記憶するデータをユニークのキーで管理するようにし、該当キャッシュが存在する場合はDBへのアクセスより、キャッシュから取り出すことを優先する

```go:controllers/article.go
package controllers

import (
	"fmt"
	"net/http"
	"strconv"
	"tkol-lite/models"
	"tkol-lite/services"

	"github.com/gin-gonic/gin"
)

func FetchArticleById(c *gin.Context) { // アイテムのIDを指定して取得
	cache_key := "article_" + c.Param("id")
	article, cache_err := models.GetCache(cache_key) // キャッシュがあれば、キャッシュから取得
	if cache_err != nil { // キャッシュ存在しない場合、DBからデータを取得して、さらにキャッシュに保存
		article, _ = services.FetchArticleById(c.Param("id"))
		models.SetCache(cache_key, article)
	}
	c.JSON(http.StatusOK, gin.H{"data": article})
}
```

## キャッシュのベースファイル
### インメモリキャッシュを使う場合
個人的に使うケースはないと思いますが、簡易のテスト実行するときにいいかもしれません。
インメモリキャッシュですので、もちろんRedisなどよりはパフォーマンスはよいですが、そもそも大量のトラフィックを捌くために、サーバーが複数台にわけることとなるため、実際のところ、実用性はないです。

```go:models/cache.go
package models

import (
	"errors"
	"time"

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

var go_cache *cache.Cache

func CreateCache() {
	c := cache.New(30*time.Minute, 60*time.Minute)
	go_cache = c
}

func SetCache(k string, v interface{}) { // キャッシュを保存
	go_cache.Set(k, v, 30*time.Second) // 保存期間を指定
}

func GetCache(k string) (interface{}, error) { // キャッシュを取得
	err := errors.New("no cache")
	r, ok := go_cache.Get(k)
	if ok {
		err = nil
	}
	return r, err
}

func DeleteCache(k string) { // キャッシュを削除
	go_cache.Delete(k)
}
```

### Redisなどのキャッシュストレージを使う場合
複数台のサーバーを利用すると、インメモリキャッシュがほとんど無意味、というよりも返ってカオスになってしまいますので、Redisなどのキャッシュストレージが必要となります。
Docker環境で試したところ、インメモリキャッシュの速度の1/3となります（当たり前ですが）。

```go:models/cache.go
package models

import (
	"time"

	"context"
	"encoding/json"
	"os"
	"github.com/joho/godotenv"
	"github.com/redis/go-redis/v9"
)

var go_cache *redis.Client
var ctx = context.Background()

func CreateCache() {
	godotenv.Load(".env") // 環境ファイルを読み込み

	go_cache = redis.NewClient(&redis.Options{
		Addr:     os.Getenv("CACHE"), // redisの接続先
		Password: "",                 // パスワード
		DB:       0,                  // デフォルトDBを使う
		PoolSize: 1000,               // サイズの上限
	})
	if err := go_cache.Ping(ctx).Err(); err != nil {
		panic(err)
	}
	go_cache.FlushAll(ctx)
}

func SetCache(k string, v interface{}) (interface{}, error) { // キャッシュを保存
	data, err := json.Marshal(v) // オブジェクト式をラッパーしたjson形式へ変換
	result, err := go_cache.Set(ctx, k, data, 0).Result()
	go_cache.Expire(ctx, k, 30*time.Second) // 保存期間を指定
	return result, err
}

func GetCache(k string) (interface{}, error) { // キャッシュを取得
	var data interface{}
	result, err := go_cache.Get(ctx, k).Result()
	json.Unmarshal([]byte(result), &data) // キャッシュに保存されたjson形式へまたオブジェクトへ変換
	return data, err
}

func DeleteCache(k string) (interface{}, error) { // キャッシュを削除
	result, err := go_cache.Del(ctx, k).Result()
	return result, err
}

```