├── .gitignore ├── go.mod ├── examples ├── memory │ └── memory_example.go ├── redis │ └── redis_example.go └── options │ └── main.go ├── persist ├── memory_test.go ├── codec.go ├── codec_test.go ├── cache.go ├── memory.go └── redis.go ├── LICENSE ├── .github └── workflows │ ├── go.yml │ └── codeql.yml ├── README_ZH.md ├── readme.md ├── option.go ├── cache.go ├── cache_test.go └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | .DS_Store 4 | .test -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/chenyahui/gin-cache 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.7.7 7 | github.com/go-redis/redis/v8 v8.11.5 8 | github.com/jellydator/ttlcache/v2 v2.11.1 9 | github.com/stretchr/testify v1.7.1 10 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 11 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 12 | ) -------------------------------------------------------------------------------- /examples/memory/memory_example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/chenyahui/gin-cache" 7 | "github.com/chenyahui/gin-cache/persist" 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | func main() { 12 | app := gin.New() 13 | 14 | memoryStore := persist.NewMemoryStore(1 * time.Minute) 15 | 16 | app.GET("/hello", 17 | cache.CacheByRequestURI(memoryStore, 2*time.Second), 18 | func(c *gin.Context) { 19 | c.String(200, "hello world") 20 | }, 21 | ) 22 | 23 | if err := app.Run(":8080"); err != nil { 24 | panic(err) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /persist/memory_test.go: -------------------------------------------------------------------------------- 1 | package persist 2 | 3 | import ( 4 | "github.com/stretchr/testify/require" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestMemoryStore(t *testing.T) { 12 | memoryStore := NewMemoryStore(1 * time.Minute) 13 | 14 | expectVal := "123" 15 | require.Nil(t, memoryStore.Set("test", expectVal, 1*time.Second)) 16 | 17 | value := "" 18 | assert.Nil(t, memoryStore.Get("test", &value)) 19 | assert.Equal(t, expectVal, value) 20 | 21 | time.Sleep(1 * time.Second) 22 | assert.Equal(t, ErrCacheMiss, memoryStore.Get("test", &value)) 23 | } 24 | -------------------------------------------------------------------------------- /persist/codec.go: -------------------------------------------------------------------------------- 1 | package persist 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | ) 7 | 8 | // Serialize returns a []byte representing the passed value 9 | func Serialize(value interface{}) ([]byte, error) { 10 | var b bytes.Buffer 11 | encoder := gob.NewEncoder(&b) 12 | if err := encoder.Encode(value); err != nil { 13 | return nil, err 14 | } 15 | return b.Bytes(), nil 16 | } 17 | 18 | // Deserialize will deserialize the passed []byte into the passed ptr interface{} 19 | func Deserialize(payload []byte, ptr interface{}) (err error) { 20 | return gob.NewDecoder(bytes.NewBuffer(payload)).Decode(ptr) 21 | } 22 | -------------------------------------------------------------------------------- /persist/codec_test.go: -------------------------------------------------------------------------------- 1 | package persist 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "github.com/stretchr/testify/require" 6 | "testing" 7 | ) 8 | 9 | type testStruct struct { 10 | A int 11 | B string 12 | C *int 13 | } 14 | 15 | func TestCodec(t *testing.T) { 16 | src := &testStruct{ 17 | A: 1, 18 | B: "2", 19 | } 20 | 21 | payload, err := Serialize(src) 22 | require.Nil(t, err) 23 | require.True(t, len(payload) > 0) 24 | 25 | var dest testStruct 26 | err = Deserialize(payload, &dest) 27 | require.Nil(t, err) 28 | 29 | assert.Equal(t, src.A, dest.A) 30 | assert.Equal(t, src.B, dest.B) 31 | assert.Equal(t, src.C, dest.C) 32 | } 33 | -------------------------------------------------------------------------------- /examples/redis/redis_example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/chenyahui/gin-cache" 7 | "github.com/chenyahui/gin-cache/persist" 8 | "github.com/gin-gonic/gin" 9 | "github.com/go-redis/redis/v8" 10 | ) 11 | 12 | func main() { 13 | app := gin.New() 14 | 15 | redisStore := persist.NewRedisStore(redis.NewClient(&redis.Options{ 16 | Network: "tcp", 17 | Addr: "127.0.0.1:6379", 18 | })) 19 | 20 | app.GET("/hello", 21 | cache.CacheByRequestURI(redisStore, 2*time.Second), 22 | func(c *gin.Context) { 23 | c.String(200, "hello world") 24 | }, 25 | ) 26 | if err := app.Run(":8080"); err != nil { 27 | panic(err) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /persist/cache.go: -------------------------------------------------------------------------------- 1 | package persist 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | ) 7 | 8 | // ErrCacheMiss represent the cache key does not exist in the store 9 | var ErrCacheMiss = errors.New("persist cache miss error") 10 | 11 | // CacheStore is the interface of a Cache backend 12 | type CacheStore interface { 13 | // Get retrieves an item from the Cache. if key does not exist in the store, return ErrCacheMiss 14 | Get(key string, value interface{}) error 15 | 16 | // Set sets an item to the Cache, replacing any existing item. 17 | Set(key string, value interface{}, expire time.Duration) error 18 | 19 | // Delete removes an item from the Cache. Does nothing if the key is not in the Cache. 20 | Delete(key string) error 21 | } 22 | -------------------------------------------------------------------------------- /examples/options/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sync/atomic" 6 | "time" 7 | 8 | cache "github.com/chenyahui/gin-cache" 9 | "github.com/chenyahui/gin-cache/persist" 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | func main() { 14 | app := gin.New() 15 | 16 | memoryStore := persist.NewMemoryStore(1 * time.Minute) 17 | 18 | var cacheHitCount, cacheMissCount int32 19 | app.GET("/hello", 20 | cache.CacheByRequestURI( 21 | memoryStore, 22 | 2*time.Second, 23 | cache.WithOnHitCache(func(c *gin.Context) { 24 | atomic.AddInt32(&cacheHitCount, 1) 25 | }), 26 | cache.WithOnMissCache(func(c *gin.Context) { 27 | atomic.AddInt32(&cacheMissCount, 1) 28 | }), 29 | ), 30 | func(c *gin.Context) { 31 | c.String(200, "hello world") 32 | }, 33 | ) 34 | 35 | app.GET("/get_hit_count", func(c *gin.Context) { 36 | c.String(200, fmt.Sprintf("total hit count: %d", cacheHitCount)) 37 | }) 38 | app.GET("/get_miss_count", func(c *gin.Context) { 39 | c.String(200, fmt.Sprintf("total miss count: %d", cacheMissCount)) 40 | }) 41 | 42 | if err := app.Run(":8080"); err != nil { 43 | panic(err) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 cyhone 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths-ignore: 7 | - '**.md' 8 | pull_request: 9 | branches: [ main ] 10 | paths-ignore: 11 | - '**.md' 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Setup go 17 | uses: actions/setup-go@v3.5.0 18 | with: 19 | go-version: '^1.13' 20 | - name: Checkout repository 21 | uses: actions/checkout@v3 22 | - name: Setup golangci-lint 23 | uses: golangci/golangci-lint-action@v3 24 | with: 25 | version: v1.54 26 | build: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v3 30 | 31 | - name: Set up Go 32 | uses: actions/setup-go@v3.5.0 33 | with: 34 | go-version: 1.13 35 | 36 | - name: Build 37 | run: go build -v ./... 38 | 39 | - name: Test 40 | run: go test -race -coverprofile=coverage.xml -v ./... 41 | 42 | - name: Upload coverage to Codecov 43 | uses: codecov/codecov-action@v2 44 | with: 45 | token: ${{secrets.CODECOV_TOKEN}} 46 | file: ./coverage.xml 47 | flags: unittests 48 | verbose: true 49 | -------------------------------------------------------------------------------- /persist/memory.go: -------------------------------------------------------------------------------- 1 | package persist 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "time" 7 | 8 | "github.com/jellydator/ttlcache/v2" 9 | ) 10 | 11 | // MemoryStore local memory cache store 12 | type MemoryStore struct { 13 | Cache *ttlcache.Cache 14 | } 15 | 16 | // NewMemoryStore allocate a local memory store with default expiration 17 | func NewMemoryStore(defaultExpiration time.Duration) *MemoryStore { 18 | cacheStore := ttlcache.NewCache() 19 | _ = cacheStore.SetTTL(defaultExpiration) 20 | 21 | // disable SkipTTLExtensionOnHit default 22 | cacheStore.SkipTTLExtensionOnHit(true) 23 | 24 | return &MemoryStore{ 25 | Cache: cacheStore, 26 | } 27 | } 28 | 29 | // Set put key value pair to memory store, and expire after expireDuration 30 | func (c *MemoryStore) Set(key string, value interface{}, expireDuration time.Duration) error { 31 | return c.Cache.SetWithTTL(key, value, expireDuration) 32 | } 33 | 34 | // Delete remove key in memory store, do nothing if key doesn't exist 35 | func (c *MemoryStore) Delete(key string) error { 36 | return c.Cache.Remove(key) 37 | } 38 | 39 | // Get key in memory store, if key doesn't exist, return ErrCacheMiss 40 | func (c *MemoryStore) Get(key string, value interface{}) error { 41 | val, err := c.Cache.Get(key) 42 | if errors.Is(err, ttlcache.ErrNotFound) { 43 | return ErrCacheMiss 44 | } 45 | 46 | v := reflect.ValueOf(value) 47 | v.Elem().Set(reflect.ValueOf(val)) 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /persist/redis.go: -------------------------------------------------------------------------------- 1 | package persist 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "time" 7 | 8 | "github.com/go-redis/redis/v8" 9 | ) 10 | 11 | // RedisStore store http response in redis 12 | type RedisStore struct { 13 | RedisClient *redis.Client 14 | } 15 | 16 | // NewRedisStore create a redis memory store with redis client 17 | func NewRedisStore(redisClient *redis.Client) *RedisStore { 18 | return &RedisStore{ 19 | RedisClient: redisClient, 20 | } 21 | } 22 | 23 | // Set put key value pair to redis, and expire after expireDuration 24 | func (store *RedisStore) Set(key string, value interface{}, expire time.Duration) error { 25 | payload, err := Serialize(value) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | ctx := context.TODO() 31 | return store.RedisClient.Set(ctx, key, payload, expire).Err() 32 | } 33 | 34 | // Delete remove key in redis, do nothing if key doesn't exist 35 | func (store *RedisStore) Delete(key string) error { 36 | ctx := context.TODO() 37 | return store.RedisClient.Del(ctx, key).Err() 38 | } 39 | 40 | // Get retrieves an item from redis, if key doesn't exist, return ErrCacheMiss 41 | func (store *RedisStore) Get(key string, value interface{}) error { 42 | ctx := context.TODO() 43 | payload, err := store.RedisClient.Get(ctx, key).Bytes() 44 | 45 | if errors.Is(err, redis.Nil) { 46 | return ErrCacheMiss 47 | } 48 | 49 | if err != nil { 50 | return err 51 | } 52 | return Deserialize(payload, value) 53 | } 54 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [ main ] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [ main ] 14 | schedule: 15 | - cron: '0 17 * * 5' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | permissions: 23 | # required for all workflows 24 | security-events: write 25 | 26 | strategy: 27 | fail-fast: false 28 | matrix: 29 | # Override automatic language detection by changing the below list 30 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 31 | # TODO: Enable for javascript later 32 | language: [ 'go'] 33 | 34 | steps: 35 | - name: Checkout repository 36 | uses: actions/checkout@v3 37 | 38 | # Initializes the CodeQL tools for scanning. 39 | - name: Initialize CodeQL 40 | uses: github/codeql-action/init@v2 41 | with: 42 | languages: ${{ matrix.language }} 43 | # If you wish to specify custom queries, you can do so here or in a config file. 44 | # By default, queries listed here will override any specified in a config file. 45 | # Prefix the list here with "+" to use these queries and those in the config file. 46 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 47 | 48 | - name: Perform CodeQL Analysis 49 | uses: github/codeql-action/analyze@v2 -------------------------------------------------------------------------------- /README_ZH.md: -------------------------------------------------------------------------------- 1 | # gin-cache 2 | [![Release](https://img.shields.io/github/release/chenyahui/gin-cache.svg?style=flat-square)](https://github.com/chenyahui/gin-cache/releases) 3 | [![doc](https://img.shields.io/badge/go.dev-doc-007d9c?style=flat-square&logo=read-the-docs)](https://pkg.go.dev/github.com/chenyahui/gin-cache) 4 | [![goreportcard for gin-cache](https://goreportcard.com/badge/github.com/chenyahui/gin-cache)](https://goreportcard.com/report/github.com/chenyahui/gin-cache) 5 | ![](https://img.shields.io/badge/license-MIT-green) 6 | [![codecov](https://codecov.io/gh/chenyahui/gin-cache/branch/main/graph/badge.svg?token=MX8Z4D5RZS)](https://codecov.io/gh/chenyahui/gin-cache) 7 | 8 | [English](README_ZH.md) | 🇨🇳中文 9 | 10 | 一个用于缓存http接口内容的gin高性能中间件。相比于官方的gin-contrib/cache,gin-cache有巨大的性能提升。 11 | 12 | # 特性 13 | * 相比于gin-contrib/cache,性能提升巨大。 14 | * 同时支持本机内存和redis作为缓存后端。 15 | * 支持用户根据请求来指定cache策略。 16 | * 使用singleflight解决了缓存击穿问题。 17 | * 仅缓存http状态码为2xx的回包 18 | 19 | # 用法 20 | 21 | ## 安装 22 | 23 | ``` 24 | go get -u github.com/chenyahui/gin-cache 25 | ``` 26 | 27 | ## 例子 28 | ## 使用本地缓存 29 | ```go 30 | package main 31 | 32 | import ( 33 | "time" 34 | 35 | "github.com/chenyahui/gin-cache" 36 | "github.com/chenyahui/gin-cache/persist" 37 | "github.com/gin-gonic/gin" 38 | ) 39 | 40 | func main() { 41 | app := gin.New() 42 | 43 | memoryStore := persist.NewMemoryStore(1 * time.Minute) 44 | 45 | app.GET("/hello", 46 | cache.CacheByRequestURI(memoryStore, 2*time.Second), 47 | func(c *gin.Context) { 48 | c.String(200, "hello world") 49 | }, 50 | ) 51 | 52 | if err := app.Run(":8080"); err != nil { 53 | panic(err) 54 | } 55 | } 56 | ``` 57 | 58 | ### 使用redis作为缓存 59 | ```go 60 | package main 61 | 62 | import ( 63 | "time" 64 | 65 | "github.com/chenyahui/gin-cache" 66 | "github.com/chenyahui/gin-cache/persist" 67 | "github.com/gin-gonic/gin" 68 | "github.com/go-redis/redis/v8" 69 | ) 70 | 71 | func main() { 72 | app := gin.New() 73 | 74 | redisStore := persist.NewRedisStore(redis.NewClient(&redis.Options{ 75 | Network: "tcp", 76 | Addr: "127.0.0.1:6379", 77 | })) 78 | 79 | app.GET("/hello", 80 | cache.CacheByRequestURI(redisStore, 2*time.Second), 81 | func(c *gin.Context) { 82 | c.String(200, "hello world") 83 | }, 84 | ) 85 | if err := app.Run(":8080"); err != nil { 86 | panic(err) 87 | } 88 | } 89 | ``` 90 | 91 | # 压测 92 | ``` 93 | wrk -c 500 -d 1m -t 5 http://127.0.0.1:8080/hello 94 | ``` 95 | 96 | ## MemoryStore 97 | 98 | ![MemoryStore QPS](https://www.cyhone.com/img/gin-cache/memory_cache_qps.png) 99 | 100 | ## RedisStore 101 | 102 | ![RedisStore QPS](https://www.cyhone.com/img/gin-cache/redis_cache_qps.png) 103 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # gin-cache 2 | [![Release](https://img.shields.io/github/release/chenyahui/gin-cache.svg?style=flat-square)](https://github.com/chenyahui/gin-cache/releases) 3 | [![doc](https://img.shields.io/badge/go.dev-doc-007d9c?style=flat-square&logo=read-the-docs)](https://pkg.go.dev/github.com/chenyahui/gin-cache) 4 | [![goreportcard for gin-cache](https://goreportcard.com/badge/github.com/chenyahui/gin-cache)](https://goreportcard.com/report/github.com/chenyahui/gin-cache) 5 | ![](https://img.shields.io/badge/license-MIT-green) 6 | [![codecov](https://codecov.io/gh/chenyahui/gin-cache/branch/main/graph/badge.svg?token=MX8Z4D5RZS)](https://codecov.io/gh/chenyahui/gin-cache) 7 | 8 | English | [🇨🇳中文](README_ZH.md) 9 | 10 | A high performance gin middleware to cache http response. Compared to gin-contrib/cache. It has a huge performance improvement. 11 | 12 | 13 | # Feature 14 | 15 | * Has a huge performance improvement compared to gin-contrib/cache. 16 | * Cache http response in local memory or Redis. 17 | * Offer a way to custom the cache strategy by per request. 18 | * Use singleflight to avoid cache breakdown problem. 19 | * Only Cache 2xx HTTP Response. 20 | 21 | # How To Use 22 | 23 | ## Install 24 | ``` 25 | go get -u github.com/chenyahui/gin-cache 26 | ``` 27 | 28 | ## Example 29 | 30 | ### Cache In Local Memory 31 | 32 | ```go 33 | package main 34 | 35 | import ( 36 | "time" 37 | 38 | "github.com/chenyahui/gin-cache" 39 | "github.com/chenyahui/gin-cache/persist" 40 | "github.com/gin-gonic/gin" 41 | ) 42 | 43 | func main() { 44 | app := gin.New() 45 | 46 | memoryStore := persist.NewMemoryStore(1 * time.Minute) 47 | 48 | app.GET("/hello", 49 | cache.CacheByRequestURI(memoryStore, 2*time.Second), 50 | func(c *gin.Context) { 51 | c.String(200, "hello world") 52 | }, 53 | ) 54 | 55 | if err := app.Run(":8080"); err != nil { 56 | panic(err) 57 | } 58 | } 59 | ``` 60 | 61 | ### Cache In Redis 62 | 63 | ```go 64 | package main 65 | 66 | import ( 67 | "time" 68 | 69 | "github.com/chenyahui/gin-cache" 70 | "github.com/chenyahui/gin-cache/persist" 71 | "github.com/gin-gonic/gin" 72 | "github.com/go-redis/redis/v8" 73 | ) 74 | 75 | func main() { 76 | app := gin.New() 77 | 78 | redisStore := persist.NewRedisStore(redis.NewClient(&redis.Options{ 79 | Network: "tcp", 80 | Addr: "127.0.0.1:6379", 81 | })) 82 | 83 | app.GET("/hello", 84 | cache.CacheByRequestURI(redisStore, 2*time.Second), 85 | func(c *gin.Context) { 86 | c.String(200, "hello world") 87 | }, 88 | ) 89 | if err := app.Run(":8080"); err != nil { 90 | panic(err) 91 | } 92 | } 93 | ``` 94 | 95 | 96 | 97 | # Benchmark 98 | 99 | ``` 100 | wrk -c 500 -d 1m -t 5 http://127.0.0.1:8080/hello 101 | ``` 102 | 103 | ## MemoryStore 104 | 105 | ![MemoryStore QPS](https://www.cyhone.com/img/gin-cache/memory_cache_qps.png) 106 | 107 | ## RedisStore 108 | 109 | ![RedisStore QPS](https://www.cyhone.com/img/gin-cache/redis_cache_qps.png) 110 | -------------------------------------------------------------------------------- /option.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // Config contains all options 10 | type Config struct { 11 | logger Logger 12 | 13 | getCacheStrategyByRequest GetCacheStrategyByRequest 14 | 15 | hitCacheCallback OnHitCacheCallback 16 | missCacheCallback OnMissCacheCallback 17 | 18 | beforeReplyWithCacheCallback BeforeReplyWithCacheCallback 19 | 20 | singleFlightForgetTimeout time.Duration 21 | shareSingleFlightCallback OnShareSingleFlightCallback 22 | 23 | ignoreQueryOrder bool 24 | prefixKey string 25 | withoutHeader bool 26 | discardHeaders []string 27 | } 28 | 29 | func newConfigByOpts(opts ...Option) *Config { 30 | cfg := &Config{ 31 | logger: Discard{}, 32 | hitCacheCallback: defaultHitCacheCallback, 33 | missCacheCallback: defaultMissCacheCallback, 34 | beforeReplyWithCacheCallback: defaultBeforeReplyWithCacheCallback, 35 | shareSingleFlightCallback: defaultShareSingleFlightCallback, 36 | } 37 | 38 | for _, opt := range opts { 39 | opt(cfg) 40 | } 41 | 42 | return cfg 43 | } 44 | 45 | // Option represents the optional function. 46 | type Option func(c *Config) 47 | 48 | // WithLogger set the custom logger 49 | func WithLogger(l Logger) Option { 50 | return func(c *Config) { 51 | if l != nil { 52 | c.logger = l 53 | } 54 | } 55 | } 56 | 57 | // Logger define the logger interface 58 | type Logger interface { 59 | Errorf(string, ...interface{}) 60 | } 61 | 62 | // Discard the default logger that will discard all logs of gin-cache 63 | type Discard struct { 64 | } 65 | 66 | // Errorf will output the log at error level 67 | func (l Discard) Errorf(string, ...interface{}) { 68 | } 69 | 70 | // WithCacheStrategyByRequest set up the custom strategy by per request 71 | func WithCacheStrategyByRequest(getGetCacheStrategyByRequest GetCacheStrategyByRequest) Option { 72 | return func(c *Config) { 73 | if getGetCacheStrategyByRequest != nil { 74 | c.getCacheStrategyByRequest = getGetCacheStrategyByRequest 75 | } 76 | } 77 | } 78 | 79 | // OnHitCacheCallback define the callback when use cache 80 | type OnHitCacheCallback func(c *gin.Context) 81 | 82 | var defaultHitCacheCallback = func(c *gin.Context) {} 83 | 84 | // WithOnHitCache will be called when cache hit. 85 | func WithOnHitCache(cb OnHitCacheCallback) Option { 86 | return func(c *Config) { 87 | if cb != nil { 88 | c.hitCacheCallback = cb 89 | } 90 | } 91 | } 92 | 93 | // OnMissCacheCallback define the callback when use cache 94 | type OnMissCacheCallback func(c *gin.Context) 95 | 96 | var defaultMissCacheCallback = func(c *gin.Context) {} 97 | 98 | // WithOnMissCache will be called when cache miss. 99 | func WithOnMissCache(cb OnMissCacheCallback) Option { 100 | return func(c *Config) { 101 | if cb != nil { 102 | c.missCacheCallback = cb 103 | } 104 | } 105 | } 106 | 107 | type BeforeReplyWithCacheCallback func(c *gin.Context, cache *ResponseCache) 108 | 109 | var defaultBeforeReplyWithCacheCallback = func(c *gin.Context, cache *ResponseCache) {} 110 | 111 | // WithBeforeReplyWithCache will be called before replying with cache. 112 | func WithBeforeReplyWithCache(cb BeforeReplyWithCacheCallback) Option { 113 | return func(c *Config) { 114 | if cb != nil { 115 | c.beforeReplyWithCacheCallback = cb 116 | } 117 | } 118 | } 119 | 120 | // OnShareSingleFlightCallback define the callback when share the singleflight result 121 | type OnShareSingleFlightCallback func(c *gin.Context) 122 | 123 | var defaultShareSingleFlightCallback = func(c *gin.Context) {} 124 | 125 | // WithOnShareSingleFlight will be called when share the singleflight result 126 | func WithOnShareSingleFlight(cb OnShareSingleFlightCallback) Option { 127 | return func(c *Config) { 128 | if cb != nil { 129 | c.shareSingleFlightCallback = cb 130 | } 131 | } 132 | } 133 | 134 | // WithSingleFlightForgetTimeout to reduce the impact of long tail requests. 135 | // singleflight.Forget will be called after the timeout has reached for each backend request when timeout is greater than zero. 136 | func WithSingleFlightForgetTimeout(forgetTimeout time.Duration) Option { 137 | return func(c *Config) { 138 | if forgetTimeout > 0 { 139 | c.singleFlightForgetTimeout = forgetTimeout 140 | } 141 | } 142 | } 143 | 144 | // IgnoreQueryOrder will ignore the queries order in url when generate cache key . This option only takes effect in CacheByRequestURI function 145 | func IgnoreQueryOrder() Option { 146 | return func(c *Config) { 147 | c.ignoreQueryOrder = true 148 | } 149 | } 150 | 151 | // WithPrefixKey will prefix the key 152 | func WithPrefixKey(prefix string) Option { 153 | return func(c *Config) { 154 | c.prefixKey = prefix 155 | } 156 | } 157 | 158 | func WithoutHeader() Option { 159 | return func(c *Config) { 160 | c.withoutHeader = true 161 | } 162 | } 163 | 164 | func WithDiscardHeaders(headers []string) Option { 165 | return func(c *Config) { 166 | c.discardHeaders = headers 167 | } 168 | } 169 | 170 | func CorsHeaders() []string { 171 | return []string{ 172 | "Access-Control-Allow-Credentials", 173 | "Access-Control-Expose-Headers", 174 | "Access-Control-Allow-Origin", 175 | "Vary", 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "errors" 7 | "net/http" 8 | "net/url" 9 | "sort" 10 | "strings" 11 | "time" 12 | 13 | "github.com/chenyahui/gin-cache/persist" 14 | "github.com/gin-gonic/gin" 15 | "golang.org/x/sync/singleflight" 16 | ) 17 | 18 | // Strategy the cache strategy 19 | type Strategy struct { 20 | CacheKey string 21 | 22 | // CacheStore if nil, use default cache store instead 23 | CacheStore persist.CacheStore 24 | 25 | // CacheDuration 26 | CacheDuration time.Duration 27 | } 28 | 29 | // GetCacheStrategyByRequest User can this function to design custom cache strategy by request. 30 | // The first return value bool means whether this request should be cached. 31 | // The second return value Strategy determine the special strategy by this request. 32 | type GetCacheStrategyByRequest func(c *gin.Context) (bool, Strategy) 33 | 34 | // Cache user must pass getCacheKey to describe the way to generate cache key 35 | func Cache( 36 | defaultCacheStore persist.CacheStore, 37 | defaultExpire time.Duration, 38 | opts ...Option, 39 | ) gin.HandlerFunc { 40 | cfg := newConfigByOpts(opts...) 41 | return cache(defaultCacheStore, defaultExpire, cfg) 42 | } 43 | 44 | func cache( 45 | defaultCacheStore persist.CacheStore, 46 | defaultExpire time.Duration, 47 | cfg *Config, 48 | ) gin.HandlerFunc { 49 | if cfg.getCacheStrategyByRequest == nil { 50 | panic("cache strategy is nil") 51 | } 52 | 53 | sfGroup := singleflight.Group{} 54 | 55 | return func(c *gin.Context) { 56 | shouldCache, cacheStrategy := cfg.getCacheStrategyByRequest(c) 57 | if !shouldCache { 58 | c.Next() 59 | return 60 | } 61 | 62 | cacheKey := cacheStrategy.CacheKey 63 | 64 | if cfg.prefixKey != "" { 65 | cacheKey = cfg.prefixKey + cacheKey 66 | } 67 | 68 | // merge cfg 69 | cacheStore := defaultCacheStore 70 | if cacheStrategy.CacheStore != nil { 71 | cacheStore = cacheStrategy.CacheStore 72 | } 73 | 74 | cacheDuration := defaultExpire 75 | if cacheStrategy.CacheDuration > 0 { 76 | cacheDuration = cacheStrategy.CacheDuration 77 | } 78 | 79 | // read cache first 80 | { 81 | respCache := &ResponseCache{} 82 | err := cacheStore.Get(cacheKey, &respCache) 83 | if err == nil { 84 | replyWithCache(c, cfg, respCache) 85 | cfg.hitCacheCallback(c) 86 | return 87 | } 88 | 89 | if !errors.Is(err, persist.ErrCacheMiss) { 90 | cfg.logger.Errorf("get cache error: %s, cache key: %s", err, cacheKey) 91 | } 92 | cfg.missCacheCallback(c) 93 | } 94 | 95 | // cache miss, then call the backend 96 | 97 | // use responseCacheWriter in order to record the response 98 | cacheWriter := &responseCacheWriter{ 99 | ResponseWriter: c.Writer, 100 | } 101 | c.Writer = cacheWriter 102 | 103 | inFlight := false 104 | rawRespCache, _, _ := sfGroup.Do(cacheKey, func() (interface{}, error) { 105 | if cfg.singleFlightForgetTimeout > 0 { 106 | forgetTimer := time.AfterFunc(cfg.singleFlightForgetTimeout, func() { 107 | sfGroup.Forget(cacheKey) 108 | }) 109 | defer forgetTimer.Stop() 110 | } 111 | 112 | c.Next() 113 | 114 | inFlight = true 115 | 116 | respCache := &ResponseCache{} 117 | respCache.fillWithCacheWriter(cacheWriter, cfg) 118 | 119 | // only cache 2xx response 120 | if !c.IsAborted() && cacheWriter.Status() < 300 && cacheWriter.Status() >= 200 { 121 | if err := cacheStore.Set(cacheKey, respCache, cacheDuration); err != nil { 122 | cfg.logger.Errorf("set cache key error: %s, cache key: %s", err, cacheKey) 123 | } 124 | } 125 | 126 | return respCache, nil 127 | }) 128 | 129 | if !inFlight { 130 | replyWithCache(c, cfg, rawRespCache.(*ResponseCache)) 131 | cfg.shareSingleFlightCallback(c) 132 | } 133 | } 134 | } 135 | 136 | // CacheByRequestURI a shortcut function for caching response by uri 137 | func CacheByRequestURI(defaultCacheStore persist.CacheStore, defaultExpire time.Duration, opts ...Option) gin.HandlerFunc { 138 | cfg := newConfigByOpts(opts...) 139 | 140 | if cfg.getCacheStrategyByRequest != nil { 141 | return cache(defaultCacheStore, defaultExpire, cfg) 142 | } 143 | 144 | var cacheStrategy GetCacheStrategyByRequest 145 | if cfg.ignoreQueryOrder { 146 | cacheStrategy = func(c *gin.Context) (bool, Strategy) { 147 | newUri, err := getRequestUriIgnoreQueryOrder(c.Request.RequestURI) 148 | if err != nil { 149 | cfg.logger.Errorf("getRequestUriIgnoreQueryOrder error: %s", err) 150 | newUri = c.Request.RequestURI 151 | } 152 | 153 | return true, Strategy{ 154 | CacheKey: newUri, 155 | } 156 | } 157 | 158 | } else { 159 | cacheStrategy = func(c *gin.Context) (bool, Strategy) { 160 | return true, Strategy{ 161 | CacheKey: c.Request.RequestURI, 162 | } 163 | } 164 | } 165 | 166 | cfg.getCacheStrategyByRequest = cacheStrategy 167 | 168 | return cache(defaultCacheStore, defaultExpire, cfg) 169 | } 170 | 171 | func CacheStrategyRequestURI(c *gin.Context) (bool, Strategy) { 172 | return true, Strategy{ 173 | CacheKey: c.Request.RequestURI, 174 | } 175 | } 176 | 177 | func CacheStrategyRequestURIIgnoreQueryOrder(c *gin.Context) (bool, Strategy) { 178 | newUri, err := getRequestUriIgnoreQueryOrder(c.Request.RequestURI) 179 | if err != nil { 180 | newUri = c.Request.RequestURI 181 | } 182 | 183 | return true, Strategy{ 184 | CacheKey: newUri, 185 | } 186 | } 187 | 188 | func getRequestUriIgnoreQueryOrder(requestURI string) (string, error) { 189 | parsedUrl, err := url.ParseRequestURI(requestURI) 190 | if err != nil { 191 | return "", err 192 | } 193 | 194 | values := parsedUrl.Query() 195 | 196 | if len(values) == 0 { 197 | return requestURI, nil 198 | } 199 | 200 | queryKeys := make([]string, 0, len(values)) 201 | for queryKey := range values { 202 | queryKeys = append(queryKeys, queryKey) 203 | } 204 | sort.Strings(queryKeys) 205 | 206 | queryVals := make([]string, 0, len(values)) 207 | for _, queryKey := range queryKeys { 208 | sort.Strings(values[queryKey]) 209 | for _, val := range values[queryKey] { 210 | queryVals = append(queryVals, queryKey+"="+val) 211 | } 212 | } 213 | 214 | return parsedUrl.Path + "?" + strings.Join(queryVals, "&"), nil 215 | } 216 | 217 | // CacheByRequestPath a shortcut function for caching response by url path, means will discard the query params 218 | func CacheByRequestPath(defaultCacheStore persist.CacheStore, defaultExpire time.Duration, opts ...Option) gin.HandlerFunc { 219 | opts = append(opts, WithCacheStrategyByRequest(func(c *gin.Context) (bool, Strategy) { 220 | return true, Strategy{ 221 | CacheKey: c.Request.URL.Path, 222 | } 223 | })) 224 | 225 | return Cache(defaultCacheStore, defaultExpire, opts...) 226 | } 227 | 228 | func init() { 229 | gob.Register(&ResponseCache{}) 230 | } 231 | 232 | // ResponseCache record the http response cache 233 | type ResponseCache struct { 234 | Status int 235 | Header http.Header 236 | Data []byte 237 | } 238 | 239 | func (c *ResponseCache) fillWithCacheWriter(cacheWriter *responseCacheWriter, cfg *Config) { 240 | c.Status = cacheWriter.Status() 241 | c.Data = cacheWriter.body.Bytes() 242 | if !cfg.withoutHeader { 243 | c.Header = cacheWriter.Header().Clone() 244 | 245 | for _, headerKey := range cfg.discardHeaders { 246 | c.Header.Del(headerKey) 247 | } 248 | } 249 | } 250 | 251 | // responseCacheWriter 252 | type responseCacheWriter struct { 253 | gin.ResponseWriter 254 | 255 | body bytes.Buffer 256 | } 257 | 258 | func (w *responseCacheWriter) Write(b []byte) (int, error) { 259 | w.body.Write(b) 260 | return w.ResponseWriter.Write(b) 261 | } 262 | 263 | func (w *responseCacheWriter) WriteString(s string) (int, error) { 264 | w.body.WriteString(s) 265 | return w.ResponseWriter.WriteString(s) 266 | } 267 | 268 | func replyWithCache( 269 | c *gin.Context, 270 | cfg *Config, 271 | respCache *ResponseCache, 272 | ) { 273 | cfg.beforeReplyWithCacheCallback(c, respCache) 274 | 275 | c.Writer.WriteHeader(respCache.Status) 276 | 277 | if !cfg.withoutHeader { 278 | for key, values := range respCache.Header { 279 | for _, val := range values { 280 | c.Writer.Header().Set(key, val) 281 | } 282 | } 283 | } 284 | 285 | if _, err := c.Writer.Write(respCache.Data); err != nil { 286 | cfg.logger.Errorf("write response error: %s", err) 287 | } 288 | 289 | // abort handler chain and return directly 290 | c.Abort() 291 | } 292 | -------------------------------------------------------------------------------- /cache_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "net/http" 7 | "net/http/httptest" 8 | "sync" 9 | "sync/atomic" 10 | "testing" 11 | "time" 12 | 13 | "github.com/chenyahui/gin-cache/persist" 14 | "github.com/gin-gonic/gin" 15 | "github.com/stretchr/testify/assert" 16 | "github.com/stretchr/testify/require" 17 | ) 18 | 19 | func init() { 20 | gin.SetMode(gin.TestMode) 21 | } 22 | 23 | func mockHttpRequest(middleware gin.HandlerFunc, url string, withRand bool) *httptest.ResponseRecorder { 24 | testWriter := httptest.NewRecorder() 25 | 26 | _, engine := gin.CreateTestContext(testWriter) 27 | engine.Use(middleware) 28 | engine.GET("/cache", func(c *gin.Context) { 29 | body := "uid:" + c.Query("uid") 30 | if withRand { 31 | body += fmt.Sprintf(",rand:%d", rand.Int()) 32 | } 33 | c.String(http.StatusOK, body) 34 | }) 35 | 36 | testRequest := httptest.NewRequest(http.MethodGet, url, nil) 37 | 38 | engine.ServeHTTP(testWriter, testRequest) 39 | 40 | return testWriter 41 | } 42 | 43 | func TestCacheByRequestPath(t *testing.T) { 44 | memoryStore := persist.NewMemoryStore(1 * time.Minute) 45 | cachePathMiddleware := CacheByRequestPath(memoryStore, 3*time.Second) 46 | 47 | w1 := mockHttpRequest(cachePathMiddleware, "/cache?uid=u1", true) 48 | w2 := mockHttpRequest(cachePathMiddleware, "/cache?uid=u2", true) 49 | w3 := mockHttpRequest(cachePathMiddleware, "/cache?uid=u3", true) 50 | 51 | assert.NotEqual(t, w1.Body, "") 52 | assert.Equal(t, w1.Body, w2.Body) 53 | assert.Equal(t, w2.Body, w3.Body) 54 | assert.Equal(t, w1.Code, w2.Code) 55 | } 56 | 57 | func TestCacheHitMissCallback(t *testing.T) { 58 | var cacheHitCount, cacheMissCount int32 59 | memoryStore := persist.NewMemoryStore(1 * time.Minute) 60 | cachePathMiddleware := CacheByRequestPath(memoryStore, 3*time.Second, 61 | WithOnHitCache(func(c *gin.Context) { 62 | atomic.AddInt32(&cacheHitCount, 1) 63 | }), 64 | WithOnMissCache(func(c *gin.Context) { 65 | atomic.AddInt32(&cacheMissCount, 1) 66 | }), 67 | ) 68 | 69 | mockHttpRequest(cachePathMiddleware, "/cache?uid=u1", true) 70 | mockHttpRequest(cachePathMiddleware, "/cache?uid=u2", true) 71 | mockHttpRequest(cachePathMiddleware, "/cache?uid=u3", true) 72 | 73 | assert.Equal(t, cacheHitCount, int32(2)) 74 | assert.Equal(t, cacheMissCount, int32(1)) 75 | } 76 | 77 | func TestCacheDuration(t *testing.T) { 78 | memoryStore := persist.NewMemoryStore(1 * time.Minute) 79 | cacheURIMiddleware := CacheByRequestURI(memoryStore, 3*time.Second) 80 | 81 | w1 := mockHttpRequest(cacheURIMiddleware, "/cache?uid=u1", true) 82 | time.Sleep(1 * time.Second) 83 | 84 | w2 := mockHttpRequest(cacheURIMiddleware, "/cache?uid=u1", true) 85 | assert.Equal(t, w1.Body, w2.Body) 86 | assert.Equal(t, w1.Code, w2.Code) 87 | time.Sleep(2 * time.Second) 88 | 89 | w3 := mockHttpRequest(cacheURIMiddleware, "/cache?uid=u1", true) 90 | assert.NotEqual(t, w1.Body, w3.Body) 91 | } 92 | 93 | func TestCacheByRequestURI(t *testing.T) { 94 | memoryStore := persist.NewMemoryStore(1 * time.Minute) 95 | cacheURIMiddleware := CacheByRequestURI(memoryStore, 3*time.Second) 96 | 97 | w1 := mockHttpRequest(cacheURIMiddleware, "/cache?uid=u1", true) 98 | w2 := mockHttpRequest(cacheURIMiddleware, "/cache?uid=u1", true) 99 | w3 := mockHttpRequest(cacheURIMiddleware, "/cache?uid=u2", true) 100 | 101 | assert.Equal(t, w1.Body, w2.Body) 102 | assert.Equal(t, w1.Code, w2.Code) 103 | 104 | assert.NotEqual(t, w2.Body, w3.Body) 105 | 106 | w4 := mockHttpRequest(cacheURIMiddleware, "/cache?uid=u4", false) 107 | assert.Equal(t, "uid:u4", w4.Body.String()) 108 | } 109 | 110 | func TestHeader(t *testing.T) { 111 | testWriter := httptest.NewRecorder() 112 | 113 | _, engine := gin.CreateTestContext(testWriter) 114 | 115 | memoryStore := persist.NewMemoryStore(1 * time.Minute) 116 | cacheURIMiddleware := CacheByRequestURI(memoryStore, 3*time.Second) 117 | 118 | engine.Use(func(c *gin.Context) { 119 | c.Header("test_header_key", "test_header_value") 120 | }) 121 | 122 | engine.Use(cacheURIMiddleware) 123 | 124 | engine.GET("/cache", func(c *gin.Context) { 125 | c.Header("test_header_key", "test_header_value2") 126 | c.String(http.StatusOK, "value") 127 | }) 128 | 129 | testRequest := httptest.NewRequest(http.MethodGet, "/cache", nil) 130 | 131 | { 132 | engine.ServeHTTP(testWriter, testRequest) 133 | value := testWriter.Header().Get("test_header_key") 134 | assert.Equal(t, "test_header_value2", value) 135 | } 136 | 137 | { 138 | engine.ServeHTTP(testWriter, testRequest) 139 | value := testWriter.Header().Get("test_header_key") 140 | assert.Equal(t, "test_header_value2", value) 141 | } 142 | } 143 | 144 | func TestConcurrentRequest(t *testing.T) { 145 | memoryStore := persist.NewMemoryStore(1 * time.Minute) 146 | cacheURIMiddleware := CacheByRequestURI(memoryStore, 1*time.Second) 147 | 148 | wg := sync.WaitGroup{} 149 | for i := 0; i < 1000; i++ { 150 | wg.Add(1) 151 | 152 | go func() { 153 | defer wg.Done() 154 | uid := rand.Intn(5) 155 | url := fmt.Sprintf("/cache?uid=%d", uid) 156 | expect := fmt.Sprintf("uid:%d", uid) 157 | 158 | writer := mockHttpRequest(cacheURIMiddleware, url, false) 159 | assert.Equal(t, expect, writer.Body.String()) 160 | }() 161 | } 162 | 163 | wg.Wait() 164 | } 165 | 166 | func TestWriteHeader(t *testing.T) { 167 | memoryStore := persist.NewMemoryStore(1 * time.Minute) 168 | cacheURIMiddleware := CacheByRequestURI(memoryStore, 1*time.Second) 169 | 170 | testWriter := httptest.NewRecorder() 171 | 172 | _, engine := gin.CreateTestContext(testWriter) 173 | engine.Use(cacheURIMiddleware) 174 | engine.GET("/cache", func(c *gin.Context) { 175 | c.Writer.WriteHeader(http.StatusOK) 176 | c.Writer.Header().Set("hello", "world") 177 | }) 178 | 179 | { 180 | testRequest := httptest.NewRequest(http.MethodGet, "/cache", nil) 181 | engine.ServeHTTP(testWriter, testRequest) 182 | assert.Equal(t, "world", testWriter.Header().Get("hello")) 183 | } 184 | 185 | { 186 | testRequest := httptest.NewRequest(http.MethodGet, "/cache", nil) 187 | engine.ServeHTTP(testWriter, testRequest) 188 | assert.Equal(t, "world", testWriter.Header().Get("hello")) 189 | } 190 | } 191 | 192 | func TestGetRequestUriIgnoreQueryOrder(t *testing.T) { 193 | val, err := getRequestUriIgnoreQueryOrder("/test?c=3&b=2&a=1") 194 | require.NoError(t, err) 195 | assert.Equal(t, "/test?a=1&b=2&c=3", val) 196 | 197 | val, err = getRequestUriIgnoreQueryOrder("/test?d=4&e=5") 198 | require.NoError(t, err) 199 | assert.Equal(t, "/test?d=4&e=5", val) 200 | } 201 | 202 | func TestCacheByRequestURIIgnoreOrder(t *testing.T) { 203 | memoryStore := persist.NewMemoryStore(1 * time.Minute) 204 | cacheURIMiddleware := CacheByRequestURI(memoryStore, 3*time.Second, IgnoreQueryOrder()) 205 | 206 | w1 := mockHttpRequest(cacheURIMiddleware, "/cache?uid=u1&a=2", true) 207 | w2 := mockHttpRequest(cacheURIMiddleware, "/cache?a=2&uid=u1", true) 208 | 209 | assert.Equal(t, w1.Body, w2.Body) 210 | assert.Equal(t, w1.Code, w2.Code) 211 | 212 | // test array query param 213 | w3 := mockHttpRequest(cacheURIMiddleware, "/cache?a=2&uid=u1&ids=1&ids=2", true) 214 | w4 := mockHttpRequest(cacheURIMiddleware, "/cache?uid=u1&a=2&ids=2&ids=1", true) 215 | 216 | assert.Equal(t, w3.Body, w4.Body) 217 | assert.Equal(t, w3.Code, w4.Code) 218 | assert.NotEqual(t, w3.Body, w1.Body) 219 | } 220 | 221 | const prefixKey = "#prefix#" 222 | 223 | func TestPrefixKey(t *testing.T) { 224 | memoryStore := persist.NewMemoryStore(1 * time.Minute) 225 | cachePathMiddleware := CacheByRequestPath( 226 | memoryStore, 227 | 3*time.Second, 228 | WithPrefixKey(prefixKey), 229 | ) 230 | 231 | requestPath := "/cache" 232 | 233 | w1 := mockHttpRequest(cachePathMiddleware, requestPath, true) 234 | 235 | err := memoryStore.Delete(prefixKey + requestPath) 236 | require.NoError(t, err) 237 | 238 | w2 := mockHttpRequest(cachePathMiddleware, requestPath, true) 239 | assert.NotEqual(t, w1.Body, w2.Body) 240 | } 241 | 242 | func TestWithDiscardHeaders(t *testing.T) { 243 | const headerKey = "RandKey" 244 | 245 | memoryStore := persist.NewMemoryStore(1 * time.Minute) 246 | cachePathMiddleware := CacheByRequestPath( 247 | memoryStore, 248 | 3*time.Second, 249 | WithDiscardHeaders([]string{ 250 | headerKey, 251 | }), 252 | ) 253 | 254 | _, engine := gin.CreateTestContext(httptest.NewRecorder()) 255 | 256 | engine.GET("/cache", cachePathMiddleware, func(c *gin.Context) { 257 | c.Header(headerKey, fmt.Sprintf("rand:%d", rand.Int())) 258 | c.String(http.StatusOK, "value") 259 | }) 260 | 261 | testRequest := httptest.NewRequest(http.MethodGet, "/cache", nil) 262 | 263 | { 264 | testWriter := httptest.NewRecorder() 265 | engine.ServeHTTP(testWriter, testRequest) 266 | headers1 := testWriter.Header() 267 | assert.NotEqual(t, headers1.Get(headerKey), "") 268 | } 269 | 270 | { 271 | testWriter := httptest.NewRecorder() 272 | engine.ServeHTTP(testWriter, testRequest) 273 | headers2 := testWriter.Header() 274 | assert.Equal(t, headers2.Get(headerKey), "") 275 | } 276 | } 277 | 278 | func TestCustomCacheStrategy(t *testing.T) { 279 | memoryStore := persist.NewMemoryStore(1 * time.Minute) 280 | cacheMiddleware := Cache( 281 | memoryStore, 282 | 24*time.Hour, 283 | WithCacheStrategyByRequest(func(c *gin.Context) (bool, Strategy) { 284 | return true, Strategy{ 285 | CacheKey: "custom_cache_key_" + c.Query("uid"), 286 | } 287 | }), 288 | ) 289 | 290 | _ = mockHttpRequest(cacheMiddleware, "/cache?uid=1", false) 291 | 292 | var val interface{} 293 | err := memoryStore.Get("custom_cache_key_1", &val) 294 | assert.Nil(t, err) 295 | } 296 | 297 | func TestCacheByRequestURICustomCacheStrategy(t *testing.T) { 298 | const customKey = "CustomKey" 299 | memoryStore := persist.NewMemoryStore(1 * time.Minute) 300 | cacheURIMiddleware := CacheByRequestURI(memoryStore, 1*time.Second, WithCacheStrategyByRequest(func(c *gin.Context) (bool, Strategy) { 301 | return true, Strategy{ 302 | CacheKey: customKey, 303 | CacheDuration: 2 * time.Second, 304 | } 305 | })) 306 | 307 | w1 := mockHttpRequest(cacheURIMiddleware, "/cache?uid=u1", true) 308 | var val interface{} 309 | err := memoryStore.Get(customKey, &val) 310 | assert.Nil(t, err) 311 | time.Sleep(1 * time.Second) 312 | 313 | w2 := mockHttpRequest(cacheURIMiddleware, "/cache?uid=u1", true) 314 | assert.Equal(t, w1.Body, w2.Body) 315 | assert.Equal(t, w1.Code, w2.Code) 316 | time.Sleep(3 * time.Second) 317 | 318 | w3 := mockHttpRequest(cacheURIMiddleware, "/cache?uid=u1", true) 319 | assert.NotEqual(t, w1.Body, w3.Body) 320 | } 321 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 2 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 3 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 4 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 5 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 10 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 11 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 12 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 13 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 14 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 15 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 16 | github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs= 17 | github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= 18 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 19 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 20 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 21 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 22 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 23 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 24 | github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= 25 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 26 | github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= 27 | github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= 28 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 29 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 30 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 31 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 32 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 33 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 34 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 35 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 36 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 37 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 38 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 39 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 40 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 41 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 42 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 43 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 44 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 45 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 46 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 47 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 48 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 49 | github.com/jellydator/ttlcache/v2 v2.11.1 h1:AZGME43Eh2Vv3giG6GeqeLeFXxwxn1/qHItqWZl6U64= 50 | github.com/jellydator/ttlcache/v2 v2.11.1/go.mod h1:RtE5Snf0/57e+2cLWFYWCCsLas2Hy3c5Z4n14XmSvTI= 51 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 52 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 53 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 54 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 55 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 56 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 57 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 58 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 59 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 60 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 61 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 62 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 63 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 64 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 65 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 66 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 67 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 68 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 69 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 70 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 71 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 72 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 73 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 74 | github.com/onsi/ginkgo/v2 v2.0.0 h1:CcuG/HvWNkkaqCUpJifQY8z7qEMBJya6aLPx6ftGyjQ= 75 | github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= 76 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 77 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 78 | github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 79 | github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= 80 | github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= 81 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 82 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 83 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 84 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 85 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 86 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 87 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 88 | github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= 89 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 90 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 91 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 92 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 93 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 94 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 95 | go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= 96 | go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= 97 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 98 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 99 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= 100 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 101 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 102 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI= 103 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 104 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 105 | golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= 106 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 107 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 108 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 109 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 110 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 111 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 112 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 113 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= 114 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 115 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 116 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 117 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 118 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 119 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= 120 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 121 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 122 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 123 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 124 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 125 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 126 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 127 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 128 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 129 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 130 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 131 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 132 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 133 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 134 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= 135 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 136 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 137 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 138 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 139 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 140 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 141 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 142 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 143 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 144 | golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 145 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 146 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 147 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 148 | golang.org/x/tools v0.0.0-20210112230658-8b4aab62c064 h1:BmCFkEH4nJrYcAc2L08yX5RhYGD4j58PTMkEUDkpz2I= 149 | golang.org/x/tools v0.0.0-20210112230658-8b4aab62c064/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 150 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 151 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 152 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 153 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 154 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 155 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 156 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 157 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 158 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 159 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 160 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 161 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 162 | google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= 163 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 164 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 165 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 166 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 167 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 168 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 169 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 170 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 171 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 172 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 173 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 174 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 175 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 176 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 177 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 178 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= --------------------------------------------------------------------------------