├── Makefile ├── CHANGELOG.md ├── .travis.yml ├── go.mod ├── bench_test.go ├── LICENSE ├── local.go ├── example_cache_test.go ├── README.md ├── cache.go ├── cache_test.go └── go.sum /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | go test ./... 3 | go test ./... -short -race 4 | go test ./... -run=NONE -bench=. -benchmem 5 | env GOOS=linux GOARCH=386 go test ./... 6 | golangci-lint run 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | > :heart: [**Uptrace.dev** - distributed traces, logs, and errors in one place](https://uptrace.dev) 4 | 5 | ## v8 6 | 7 | - Added s2 (snappy) compression. That means that v8 can't read the data set by v7. 8 | - Replaced LRU with TinyLFU for local cache. 9 | - Requires go-redis v8. 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | services: 4 | - redis-server 5 | 6 | go: 7 | - 1.14.x 8 | - 1.15.x 9 | - tip 10 | 11 | matrix: 12 | allow_failures: 13 | - go: tip 14 | 15 | env: 16 | - GO111MODULE=on 17 | 18 | go_import_path: github.com/go-redis/cache 19 | 20 | before_install: 21 | - redis-server --port 6380 & 22 | - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.21.0 23 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-redis/cache/v8 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/davecgh/go-spew v1.1.1 // indirect 7 | github.com/go-redis/redis/v8 v8.4.4 8 | github.com/golang/protobuf v1.4.3 // indirect 9 | github.com/klauspost/compress v1.12.2 10 | github.com/onsi/ginkgo v1.14.2 11 | github.com/onsi/gomega v1.10.4 12 | github.com/vmihailenco/bufpool v0.1.11 13 | github.com/vmihailenco/go-tinylfu v0.2.1 14 | github.com/vmihailenco/msgpack/v5 v5.1.0 15 | golang.org/x/exp v0.0.0-20201221025956-e89b829e73ea 16 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a 17 | google.golang.org/protobuf v1.25.0 // indirect 18 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | package cache_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/go-redis/cache/v8" 8 | ) 9 | 10 | func BenchmarkOnce(b *testing.B) { 11 | mycache := newCacheWithLocal(newRing()) 12 | obj := &Object{ 13 | Str: strings.Repeat("my very large string", 10), 14 | Num: 42, 15 | } 16 | 17 | b.ResetTimer() 18 | 19 | b.RunParallel(func(pb *testing.PB) { 20 | for pb.Next() { 21 | var dst Object 22 | err := mycache.Once(&cache.Item{ 23 | Key: "bench-once", 24 | Value: &dst, 25 | Do: func(*cache.Item) (interface{}, error) { 26 | return obj, nil 27 | }, 28 | }) 29 | if err != nil { 30 | b.Fatal(err) 31 | } 32 | if dst.Num != 42 { 33 | b.Fatalf("%d != 42", dst.Num) 34 | } 35 | } 36 | }) 37 | } 38 | 39 | func BenchmarkSet(b *testing.B) { 40 | mycache := newCacheWithLocal(newRing()) 41 | obj := &Object{ 42 | Str: strings.Repeat("my very large string", 10), 43 | Num: 42, 44 | } 45 | 46 | b.ResetTimer() 47 | 48 | b.RunParallel(func(pb *testing.PB) { 49 | for pb.Next() { 50 | if err := mycache.Set(&cache.Item{ 51 | Key: "bench-set", 52 | Value: obj, 53 | }); err != nil { 54 | b.Fatal(err) 55 | } 56 | } 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 The github.com/go-redis/cache Contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the 13 | distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 18 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 19 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 21 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 23 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /local.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/vmihailenco/go-tinylfu" 8 | "golang.org/x/exp/rand" 9 | ) 10 | 11 | type LocalCache interface { 12 | Set(key string, data []byte) 13 | Get(key string) ([]byte, bool) 14 | Del(key string) 15 | } 16 | 17 | type TinyLFU struct { 18 | mu sync.Mutex 19 | rand *rand.Rand 20 | lfu *tinylfu.T 21 | ttl time.Duration 22 | offset time.Duration 23 | } 24 | 25 | var _ LocalCache = (*TinyLFU)(nil) 26 | 27 | func NewTinyLFU(size int, ttl time.Duration) *TinyLFU { 28 | const maxOffset = 10 * time.Second 29 | 30 | offset := ttl / 10 31 | if offset > maxOffset { 32 | offset = maxOffset 33 | } 34 | 35 | return &TinyLFU{ 36 | rand: rand.New(rand.NewSource(uint64(time.Now().UnixNano()))), 37 | lfu: tinylfu.New(size, 100000), 38 | ttl: ttl, 39 | offset: offset, 40 | } 41 | } 42 | 43 | func (c *TinyLFU) UseRandomizedTTL(offset time.Duration) { 44 | c.offset = offset 45 | } 46 | 47 | func (c *TinyLFU) Set(key string, b []byte) { 48 | c.mu.Lock() 49 | defer c.mu.Unlock() 50 | 51 | ttl := c.ttl 52 | if c.offset > 0 { 53 | ttl += time.Duration(c.rand.Int63n(int64(c.offset))) 54 | } 55 | 56 | c.lfu.Set(&tinylfu.Item{ 57 | Key: key, 58 | Value: b, 59 | ExpireAt: time.Now().Add(ttl), 60 | }) 61 | } 62 | 63 | func (c *TinyLFU) Get(key string) ([]byte, bool) { 64 | c.mu.Lock() 65 | defer c.mu.Unlock() 66 | 67 | val, ok := c.lfu.Get(key) 68 | if !ok { 69 | return nil, false 70 | } 71 | 72 | b := val.([]byte) 73 | return b, true 74 | } 75 | 76 | func (c *TinyLFU) Del(key string) { 77 | c.mu.Lock() 78 | defer c.mu.Unlock() 79 | 80 | c.lfu.Del(key) 81 | } 82 | -------------------------------------------------------------------------------- /example_cache_test.go: -------------------------------------------------------------------------------- 1 | package cache_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/go-redis/redis/v8" 9 | 10 | "github.com/go-redis/cache/v8" 11 | ) 12 | 13 | type Object struct { 14 | Str string 15 | Num int 16 | } 17 | 18 | func Example_basicUsage() { 19 | ring := redis.NewRing(&redis.RingOptions{ 20 | Addrs: map[string]string{ 21 | "server1": ":6379", 22 | "server2": ":6380", 23 | }, 24 | }) 25 | 26 | mycache := cache.New(&cache.Options{ 27 | Redis: ring, 28 | LocalCache: cache.NewTinyLFU(1000, time.Minute), 29 | }) 30 | 31 | ctx := context.TODO() 32 | key := "mykey" 33 | obj := &Object{ 34 | Str: "mystring", 35 | Num: 42, 36 | } 37 | 38 | if err := mycache.Set(&cache.Item{ 39 | Ctx: ctx, 40 | Key: key, 41 | Value: obj, 42 | TTL: time.Hour, 43 | }); err != nil { 44 | panic(err) 45 | } 46 | 47 | var wanted Object 48 | if err := mycache.Get(ctx, key, &wanted); err == nil { 49 | fmt.Println(wanted) 50 | } 51 | 52 | // Output: {mystring 42} 53 | } 54 | 55 | func Example_advancedUsage() { 56 | ring := redis.NewRing(&redis.RingOptions{ 57 | Addrs: map[string]string{ 58 | "server1": ":6379", 59 | "server2": ":6380", 60 | }, 61 | }) 62 | 63 | mycache := cache.New(&cache.Options{ 64 | Redis: ring, 65 | LocalCache: cache.NewTinyLFU(1000, time.Minute), 66 | }) 67 | 68 | obj := new(Object) 69 | err := mycache.Once(&cache.Item{ 70 | Key: "mykey", 71 | Value: obj, // destination 72 | Do: func(*cache.Item) (interface{}, error) { 73 | return &Object{ 74 | Str: "mystring", 75 | Num: 42, 76 | }, nil 77 | }, 78 | }) 79 | if err != nil { 80 | panic(err) 81 | } 82 | fmt.Println(obj) 83 | // Output: &{mystring 42} 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redis cache library for Golang 2 | 3 | [![Build Status](https://travis-ci.org/go-redis/cache.svg)](https://travis-ci.org/go-redis/cache) 4 | [![GoDoc](https://godoc.org/github.com/go-redis/cache?status.svg)](https://pkg.go.dev/github.com/go-redis/cache/v8?tab=doc) 5 | 6 | go-redis/cache library implements a cache using Redis as a 7 | key/value storage. It uses [MessagePack](https://github.com/vmihailenco/msgpack) to marshal values. 8 | 9 | Optinally you can use [TinyLFU](https://github.com/dgryski/go-tinylfu) or any other [cache algorithm](https://github.com/vmihailenco/go-cache-benchmark) as a local in-process cache. 10 | 11 | ## Installation 12 | 13 | go-redis/cache supports 2 last Go versions and requires a Go version with 14 | [modules](https://github.com/golang/go/wiki/Modules) support. So make sure to initialize a Go 15 | module: 16 | 17 | ```shell 18 | go mod init github.com/my/repo 19 | ``` 20 | 21 | And then install go-redis/cache/v8 (note _v8_ in the import; omitting it is a popular mistake): 22 | 23 | ```shell 24 | go get github.com/go-redis/cache/v8 25 | ``` 26 | 27 | ## Quickstart 28 | 29 | ```go 30 | package cache_test 31 | 32 | import ( 33 | "context" 34 | "fmt" 35 | "time" 36 | 37 | "github.com/go-redis/redis/v8" 38 | "github.com/go-redis/cache/v8" 39 | ) 40 | 41 | type Object struct { 42 | Str string 43 | Num int 44 | } 45 | 46 | func Example_basicUsage() { 47 | ring := redis.NewRing(&redis.RingOptions{ 48 | Addrs: map[string]string{ 49 | "server1": ":6379", 50 | "server2": ":6380", 51 | }, 52 | }) 53 | 54 | mycache := cache.New(&cache.Options{ 55 | Redis: ring, 56 | LocalCache: cache.NewTinyLFU(1000, time.Minute), 57 | }) 58 | 59 | ctx := context.TODO() 60 | key := "mykey" 61 | obj := &Object{ 62 | Str: "mystring", 63 | Num: 42, 64 | } 65 | 66 | if err := mycache.Set(&cache.Item{ 67 | Ctx: ctx, 68 | Key: key, 69 | Value: obj, 70 | TTL: time.Hour, 71 | }); err != nil { 72 | panic(err) 73 | } 74 | 75 | var wanted Object 76 | if err := mycache.Get(ctx, key, &wanted); err == nil { 77 | fmt.Println(wanted) 78 | } 79 | 80 | // Output: {mystring 42} 81 | } 82 | 83 | func Example_advancedUsage() { 84 | ring := redis.NewRing(&redis.RingOptions{ 85 | Addrs: map[string]string{ 86 | "server1": ":6379", 87 | "server2": ":6380", 88 | }, 89 | }) 90 | 91 | mycache := cache.New(&cache.Options{ 92 | Redis: ring, 93 | LocalCache: cache.NewTinyLFU(1000, time.Minute), 94 | }) 95 | 96 | obj := new(Object) 97 | err := mycache.Once(&cache.Item{ 98 | Key: "mykey", 99 | Value: obj, // destination 100 | Do: func(*cache.Item) (interface{}, error) { 101 | return &Object{ 102 | Str: "mystring", 103 | Num: 42, 104 | }, nil 105 | }, 106 | }) 107 | if err != nil { 108 | panic(err) 109 | } 110 | fmt.Println(obj) 111 | // Output: &{mystring 42} 112 | } 113 | ``` 114 | -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "sync/atomic" 9 | "time" 10 | 11 | "github.com/go-redis/redis/v8" 12 | "github.com/klauspost/compress/s2" 13 | "github.com/vmihailenco/bufpool" 14 | "github.com/vmihailenco/msgpack/v5" 15 | "golang.org/x/sync/singleflight" 16 | ) 17 | 18 | const ( 19 | compressionThreshold = 64 20 | timeLen = 4 21 | ) 22 | 23 | const ( 24 | noCompression = 0x0 25 | s2Compression = 0x1 26 | ) 27 | 28 | var ( 29 | ErrCacheMiss = errors.New("cache: key is missing") 30 | errRedisLocalCacheNil = errors.New("cache: both Redis and LocalCache are nil") 31 | ) 32 | 33 | type rediser interface { 34 | Set(ctx context.Context, key string, value interface{}, ttl time.Duration) *redis.StatusCmd 35 | SetXX(ctx context.Context, key string, value interface{}, ttl time.Duration) *redis.BoolCmd 36 | SetNX(ctx context.Context, key string, value interface{}, ttl time.Duration) *redis.BoolCmd 37 | 38 | Get(ctx context.Context, key string) *redis.StringCmd 39 | Del(ctx context.Context, keys ...string) *redis.IntCmd 40 | } 41 | 42 | type Item struct { 43 | Ctx context.Context 44 | 45 | Key string 46 | Value interface{} 47 | 48 | // TTL is the cache expiration time. 49 | // Default TTL is 1 hour. 50 | TTL time.Duration 51 | 52 | // Do returns value to be cached. 53 | Do func(*Item) (interface{}, error) 54 | 55 | // SetXX only sets the key if it already exists. 56 | SetXX bool 57 | 58 | // SetNX only sets the key if it does not already exist. 59 | SetNX bool 60 | 61 | // SkipLocalCache skips local cache as if it is not set. 62 | SkipLocalCache bool 63 | } 64 | 65 | func (item *Item) Context() context.Context { 66 | if item.Ctx == nil { 67 | return context.Background() 68 | } 69 | return item.Ctx 70 | } 71 | 72 | func (item *Item) value() (interface{}, error) { 73 | if item.Do != nil { 74 | return item.Do(item) 75 | } 76 | if item.Value != nil { 77 | return item.Value, nil 78 | } 79 | return nil, nil 80 | } 81 | 82 | func (item *Item) ttl() time.Duration { 83 | const defaultTTL = time.Hour 84 | 85 | if item.TTL < 0 { 86 | return 0 87 | } 88 | 89 | if item.TTL != 0 { 90 | if item.TTL < time.Second { 91 | log.Printf("too short TTL for key=%q: %s", item.Key, item.TTL) 92 | return defaultTTL 93 | } 94 | return item.TTL 95 | } 96 | 97 | return defaultTTL 98 | } 99 | 100 | //------------------------------------------------------------------------------ 101 | type MarshalFunc func(interface{}) ([]byte, error) 102 | type UnmarshalFunc func([]byte, interface{}) error 103 | 104 | type Options struct { 105 | Redis rediser 106 | LocalCache LocalCache 107 | StatsEnabled bool 108 | Marshal MarshalFunc 109 | Unmarshal UnmarshalFunc 110 | } 111 | 112 | type Cache struct { 113 | opt *Options 114 | 115 | group singleflight.Group 116 | bufpool bufpool.Pool 117 | 118 | marshal MarshalFunc 119 | unmarshal UnmarshalFunc 120 | 121 | hits uint64 122 | misses uint64 123 | } 124 | 125 | func New(opt *Options) *Cache { 126 | cacher := &Cache{ 127 | opt: opt, 128 | } 129 | 130 | if opt.Marshal == nil { 131 | cacher.marshal = cacher._marshal 132 | } else { 133 | cacher.marshal = opt.Marshal 134 | } 135 | 136 | if opt.Unmarshal == nil { 137 | cacher.unmarshal = cacher._unmarshal 138 | } else { 139 | cacher.unmarshal = opt.Unmarshal 140 | } 141 | return cacher 142 | } 143 | 144 | // Set caches the item. 145 | func (cd *Cache) Set(item *Item) error { 146 | _, _, err := cd.set(item) 147 | return err 148 | } 149 | 150 | func (cd *Cache) set(item *Item) ([]byte, bool, error) { 151 | value, err := item.value() 152 | if err != nil { 153 | return nil, false, err 154 | } 155 | 156 | b, err := cd.Marshal(value) 157 | if err != nil { 158 | return nil, false, err 159 | } 160 | 161 | if cd.opt.LocalCache != nil && !item.SkipLocalCache { 162 | cd.opt.LocalCache.Set(item.Key, b) 163 | } 164 | 165 | if cd.opt.Redis == nil { 166 | if cd.opt.LocalCache == nil { 167 | return b, true, errRedisLocalCacheNil 168 | } 169 | return b, true, nil 170 | } 171 | 172 | ttl := item.ttl() 173 | if ttl == 0 { 174 | return b, true, nil 175 | } 176 | 177 | if item.SetXX { 178 | return b, true, cd.opt.Redis.SetXX(item.Context(), item.Key, b, ttl).Err() 179 | } 180 | if item.SetNX { 181 | return b, true, cd.opt.Redis.SetNX(item.Context(), item.Key, b, ttl).Err() 182 | } 183 | return b, true, cd.opt.Redis.Set(item.Context(), item.Key, b, ttl).Err() 184 | } 185 | 186 | // Exists reports whether value for the given key exists. 187 | func (cd *Cache) Exists(ctx context.Context, key string) bool { 188 | return cd.Get(ctx, key, nil) == nil 189 | } 190 | 191 | // Get gets the value for the given key. 192 | func (cd *Cache) Get(ctx context.Context, key string, value interface{}) error { 193 | return cd.get(ctx, key, value, false) 194 | } 195 | 196 | // Get gets the value for the given key skipping local cache. 197 | func (cd *Cache) GetSkippingLocalCache( 198 | ctx context.Context, key string, value interface{}, 199 | ) error { 200 | return cd.get(ctx, key, value, true) 201 | } 202 | 203 | func (cd *Cache) get( 204 | ctx context.Context, 205 | key string, 206 | value interface{}, 207 | skipLocalCache bool, 208 | ) error { 209 | b, err := cd.getBytes(ctx, key, skipLocalCache) 210 | if err != nil { 211 | return err 212 | } 213 | return cd.unmarshal(b, value) 214 | } 215 | 216 | func (cd *Cache) getBytes(ctx context.Context, key string, skipLocalCache bool) ([]byte, error) { 217 | if !skipLocalCache && cd.opt.LocalCache != nil { 218 | b, ok := cd.opt.LocalCache.Get(key) 219 | if ok { 220 | return b, nil 221 | } 222 | } 223 | 224 | if cd.opt.Redis == nil { 225 | if cd.opt.LocalCache == nil { 226 | return nil, errRedisLocalCacheNil 227 | } 228 | return nil, ErrCacheMiss 229 | } 230 | 231 | b, err := cd.opt.Redis.Get(ctx, key).Bytes() 232 | if err != nil { 233 | if cd.opt.StatsEnabled { 234 | atomic.AddUint64(&cd.misses, 1) 235 | } 236 | if err == redis.Nil { 237 | return nil, ErrCacheMiss 238 | } 239 | return nil, err 240 | } 241 | 242 | if cd.opt.StatsEnabled { 243 | atomic.AddUint64(&cd.hits, 1) 244 | } 245 | 246 | if !skipLocalCache && cd.opt.LocalCache != nil { 247 | cd.opt.LocalCache.Set(key, b) 248 | } 249 | return b, nil 250 | } 251 | 252 | // Once gets the item.Value for the given item.Key from the cache or 253 | // executes, caches, and returns the results of the given item.Func, 254 | // making sure that only one execution is in-flight for a given item.Key 255 | // at a time. If a duplicate comes in, the duplicate caller waits for the 256 | // original to complete and receives the same results. 257 | func (cd *Cache) Once(item *Item) error { 258 | b, cached, err := cd.getSetItemBytesOnce(item) 259 | if err != nil { 260 | return err 261 | } 262 | 263 | if item.Value == nil || len(b) == 0 { 264 | return nil 265 | } 266 | 267 | if err := cd.unmarshal(b, item.Value); err != nil { 268 | if cached { 269 | _ = cd.Delete(item.Context(), item.Key) 270 | return cd.Once(item) 271 | } 272 | return err 273 | } 274 | 275 | return nil 276 | } 277 | 278 | func (cd *Cache) getSetItemBytesOnce(item *Item) (b []byte, cached bool, err error) { 279 | if cd.opt.LocalCache != nil { 280 | b, ok := cd.opt.LocalCache.Get(item.Key) 281 | if ok { 282 | return b, true, nil 283 | } 284 | } 285 | 286 | v, err, _ := cd.group.Do(item.Key, func() (interface{}, error) { 287 | b, err := cd.getBytes(item.Context(), item.Key, item.SkipLocalCache) 288 | if err == nil { 289 | cached = true 290 | return b, nil 291 | } 292 | 293 | b, ok, err := cd.set(item) 294 | if ok { 295 | return b, nil 296 | } 297 | return nil, err 298 | }) 299 | if err != nil { 300 | return nil, false, err 301 | } 302 | return v.([]byte), cached, nil 303 | } 304 | 305 | func (cd *Cache) Delete(ctx context.Context, key string) error { 306 | if cd.opt.LocalCache != nil { 307 | cd.opt.LocalCache.Del(key) 308 | } 309 | 310 | if cd.opt.Redis == nil { 311 | if cd.opt.LocalCache == nil { 312 | return errRedisLocalCacheNil 313 | } 314 | return nil 315 | } 316 | 317 | _, err := cd.opt.Redis.Del(ctx, key).Result() 318 | return err 319 | } 320 | 321 | func (cd *Cache) DeleteFromLocalCache(key string) { 322 | if cd.opt.LocalCache != nil { 323 | cd.opt.LocalCache.Del(key) 324 | } 325 | } 326 | 327 | func (cd *Cache) Marshal(value interface{}) ([]byte, error) { 328 | return cd.marshal(value) 329 | } 330 | 331 | func (cd *Cache) _marshal(value interface{}) ([]byte, error) { 332 | switch value := value.(type) { 333 | case nil: 334 | return nil, nil 335 | case []byte: 336 | return value, nil 337 | case string: 338 | return []byte(value), nil 339 | } 340 | 341 | buf := cd.bufpool.Get() 342 | defer cd.bufpool.Put(buf) 343 | 344 | enc := msgpack.GetEncoder() 345 | enc.Reset(buf) 346 | enc.UseCompactInts(true) 347 | 348 | err := enc.Encode(value) 349 | 350 | msgpack.PutEncoder(enc) 351 | 352 | if err != nil { 353 | return nil, err 354 | } 355 | 356 | return compress(buf.Bytes()), nil 357 | } 358 | 359 | func compress(data []byte) []byte { 360 | if len(data) < compressionThreshold { 361 | n := len(data) + 1 362 | b := make([]byte, n, n+timeLen) 363 | copy(b, data) 364 | b[len(b)-1] = noCompression 365 | return b 366 | } 367 | 368 | n := s2.MaxEncodedLen(len(data)) + 1 369 | b := make([]byte, n, n+timeLen) 370 | b = s2.Encode(b, data) 371 | b = append(b, s2Compression) 372 | return b 373 | } 374 | 375 | func (cd *Cache) Unmarshal(b []byte, value interface{}) error { 376 | return cd.unmarshal(b, value) 377 | } 378 | 379 | func (cd *Cache) _unmarshal(b []byte, value interface{}) error { 380 | if len(b) == 0 { 381 | return nil 382 | } 383 | 384 | switch value := value.(type) { 385 | case nil: 386 | return nil 387 | case *[]byte: 388 | clone := make([]byte, len(b)) 389 | copy(clone, b) 390 | *value = clone 391 | return nil 392 | case *string: 393 | *value = string(b) 394 | return nil 395 | } 396 | 397 | switch c := b[len(b)-1]; c { 398 | case noCompression: 399 | b = b[:len(b)-1] 400 | case s2Compression: 401 | b = b[:len(b)-1] 402 | 403 | n, err := s2.DecodedLen(b) 404 | if err != nil { 405 | return err 406 | } 407 | 408 | buf := bufpool.Get(n) 409 | defer bufpool.Put(buf) 410 | 411 | b, err = s2.Decode(buf.Bytes(), b) 412 | if err != nil { 413 | return err 414 | } 415 | default: 416 | return fmt.Errorf("unknown compression method: %x", c) 417 | } 418 | 419 | return msgpack.Unmarshal(b, value) 420 | } 421 | 422 | //------------------------------------------------------------------------------ 423 | 424 | type Stats struct { 425 | Hits uint64 426 | Misses uint64 427 | } 428 | 429 | // Stats returns cache statistics. 430 | func (cd *Cache) Stats() *Stats { 431 | if !cd.opt.StatsEnabled { 432 | return nil 433 | } 434 | return &Stats{ 435 | Hits: atomic.LoadUint64(&cd.hits), 436 | Misses: atomic.LoadUint64(&cd.misses), 437 | } 438 | } 439 | -------------------------------------------------------------------------------- /cache_test.go: -------------------------------------------------------------------------------- 1 | package cache_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "sync" 8 | "sync/atomic" 9 | "testing" 10 | "time" 11 | 12 | "github.com/go-redis/redis/v8" 13 | . "github.com/onsi/ginkgo" 14 | . "github.com/onsi/gomega" 15 | 16 | "github.com/go-redis/cache/v8" 17 | ) 18 | 19 | func TestGinkgo(t *testing.T) { 20 | RegisterFailHandler(Fail) 21 | RunSpecs(t, "cache") 22 | } 23 | 24 | func perform(n int, cbs ...func(int)) { 25 | var wg sync.WaitGroup 26 | for _, cb := range cbs { 27 | for i := 0; i < n; i++ { 28 | wg.Add(1) 29 | go func(cb func(int), i int) { 30 | defer wg.Done() 31 | defer GinkgoRecover() 32 | 33 | cb(i) 34 | }(cb, i) 35 | } 36 | } 37 | wg.Wait() 38 | } 39 | 40 | var _ = Describe("Cache", func() { 41 | ctx := context.TODO() 42 | 43 | const key = "mykey" 44 | var obj *Object 45 | 46 | var rdb *redis.Ring 47 | var mycache *cache.Cache 48 | 49 | testCache := func() { 50 | It("Gets and Sets nil", func() { 51 | err := mycache.Set(&cache.Item{ 52 | Key: key, 53 | TTL: time.Hour, 54 | }) 55 | Expect(err).NotTo(HaveOccurred()) 56 | 57 | err = mycache.Get(ctx, key, nil) 58 | Expect(err).NotTo(HaveOccurred()) 59 | 60 | Expect(mycache.Exists(ctx, key)).To(BeTrue()) 61 | }) 62 | 63 | It("Deletes key", func() { 64 | err := mycache.Set(&cache.Item{ 65 | Ctx: ctx, 66 | Key: key, 67 | TTL: time.Hour, 68 | }) 69 | Expect(err).NotTo(HaveOccurred()) 70 | 71 | Expect(mycache.Exists(ctx, key)).To(BeTrue()) 72 | 73 | err = mycache.Delete(ctx, key) 74 | Expect(err).NotTo(HaveOccurred()) 75 | 76 | err = mycache.Get(ctx, key, nil) 77 | Expect(err).To(Equal(cache.ErrCacheMiss)) 78 | 79 | Expect(mycache.Exists(ctx, key)).To(BeFalse()) 80 | }) 81 | 82 | It("Gets and Sets data", func() { 83 | err := mycache.Set(&cache.Item{ 84 | Ctx: ctx, 85 | Key: key, 86 | Value: obj, 87 | TTL: time.Hour, 88 | }) 89 | Expect(err).NotTo(HaveOccurred()) 90 | 91 | wanted := new(Object) 92 | err = mycache.Get(ctx, key, wanted) 93 | Expect(err).NotTo(HaveOccurred()) 94 | Expect(wanted).To(Equal(obj)) 95 | 96 | Expect(mycache.Exists(ctx, key)).To(BeTrue()) 97 | }) 98 | 99 | It("Sets string as is", func() { 100 | value := "str_value" 101 | 102 | err := mycache.Set(&cache.Item{ 103 | Ctx: ctx, 104 | Key: key, 105 | Value: value, 106 | }) 107 | Expect(err).NotTo(HaveOccurred()) 108 | 109 | var dst string 110 | err = mycache.Get(ctx, key, &dst) 111 | Expect(err).NotTo(HaveOccurred()) 112 | Expect(dst).To(Equal(value)) 113 | }) 114 | 115 | It("Sets bytes as is", func() { 116 | value := []byte("str_value") 117 | 118 | err := mycache.Set(&cache.Item{ 119 | Ctx: ctx, 120 | Key: key, 121 | Value: value, 122 | }) 123 | Expect(err).NotTo(HaveOccurred()) 124 | 125 | var dst []byte 126 | err = mycache.Get(ctx, key, &dst) 127 | Expect(err).NotTo(HaveOccurred()) 128 | Expect(dst).To(Equal(value)) 129 | }) 130 | 131 | It("can be used with Incr", func() { 132 | if rdb == nil { 133 | return 134 | } 135 | 136 | value := "123" 137 | 138 | err := mycache.Set(&cache.Item{ 139 | Ctx: ctx, 140 | Key: key, 141 | Value: value, 142 | }) 143 | Expect(err).NotTo(HaveOccurred()) 144 | 145 | n, err := rdb.Incr(ctx, key).Result() 146 | Expect(err).NotTo(HaveOccurred()) 147 | Expect(n).To(Equal(int64(124))) 148 | }) 149 | 150 | Describe("Once func", func() { 151 | It("calls Func when cache fails", func() { 152 | err := mycache.Set(&cache.Item{ 153 | Ctx: ctx, 154 | Key: key, 155 | Value: int64(0), 156 | }) 157 | Expect(err).NotTo(HaveOccurred()) 158 | 159 | var got bool 160 | err = mycache.Get(ctx, key, &got) 161 | Expect(err).To(MatchError("msgpack: invalid code=0 decoding bool")) 162 | 163 | err = mycache.Once(&cache.Item{ 164 | Ctx: ctx, 165 | Key: key, 166 | Value: &got, 167 | Do: func(*cache.Item) (interface{}, error) { 168 | return true, nil 169 | }, 170 | }) 171 | Expect(err).NotTo(HaveOccurred()) 172 | Expect(got).To(BeTrue()) 173 | 174 | got = false 175 | err = mycache.Get(ctx, key, &got) 176 | Expect(err).NotTo(HaveOccurred()) 177 | Expect(got).To(BeTrue()) 178 | }) 179 | 180 | It("does not cache when Func fails", func() { 181 | perform(100, func(int) { 182 | var got bool 183 | err := mycache.Once(&cache.Item{ 184 | Ctx: ctx, 185 | Key: key, 186 | Value: &got, 187 | Do: func(*cache.Item) (interface{}, error) { 188 | return nil, io.EOF 189 | }, 190 | }) 191 | Expect(err).To(Equal(io.EOF)) 192 | Expect(got).To(BeFalse()) 193 | }) 194 | 195 | var got bool 196 | err := mycache.Get(ctx, key, &got) 197 | Expect(err).To(Equal(cache.ErrCacheMiss)) 198 | 199 | err = mycache.Once(&cache.Item{ 200 | Ctx: ctx, 201 | Key: key, 202 | Value: &got, 203 | Do: func(*cache.Item) (interface{}, error) { 204 | return true, nil 205 | }, 206 | }) 207 | Expect(err).NotTo(HaveOccurred()) 208 | Expect(got).To(BeTrue()) 209 | }) 210 | 211 | It("works with Value", func() { 212 | var callCount int64 213 | perform(100, func(int) { 214 | got := new(Object) 215 | err := mycache.Once(&cache.Item{ 216 | Ctx: ctx, 217 | Key: key, 218 | Value: got, 219 | Do: func(*cache.Item) (interface{}, error) { 220 | atomic.AddInt64(&callCount, 1) 221 | return obj, nil 222 | }, 223 | }) 224 | Expect(err).NotTo(HaveOccurred()) 225 | Expect(got).To(Equal(obj)) 226 | }) 227 | Expect(callCount).To(Equal(int64(1))) 228 | }) 229 | 230 | It("works with ptr and non-ptr", func() { 231 | var callCount int64 232 | perform(100, func(int) { 233 | got := new(Object) 234 | err := mycache.Once(&cache.Item{ 235 | Ctx: ctx, 236 | Key: key, 237 | Value: got, 238 | Do: func(*cache.Item) (interface{}, error) { 239 | atomic.AddInt64(&callCount, 1) 240 | return *obj, nil 241 | }, 242 | }) 243 | Expect(err).NotTo(HaveOccurred()) 244 | Expect(got).To(Equal(obj)) 245 | }) 246 | Expect(callCount).To(Equal(int64(1))) 247 | }) 248 | 249 | It("works with bool", func() { 250 | var callCount int64 251 | perform(100, func(int) { 252 | var got bool 253 | err := mycache.Once(&cache.Item{ 254 | Ctx: ctx, 255 | Key: key, 256 | Value: &got, 257 | Do: func(*cache.Item) (interface{}, error) { 258 | atomic.AddInt64(&callCount, 1) 259 | return true, nil 260 | }, 261 | }) 262 | Expect(err).NotTo(HaveOccurred()) 263 | Expect(got).To(BeTrue()) 264 | }) 265 | Expect(callCount).To(Equal(int64(1))) 266 | }) 267 | 268 | It("works without Value and nil result", func() { 269 | var callCount int64 270 | perform(100, func(int) { 271 | err := mycache.Once(&cache.Item{ 272 | Ctx: ctx, 273 | Key: key, 274 | Do: func(*cache.Item) (interface{}, error) { 275 | atomic.AddInt64(&callCount, 1) 276 | return nil, nil 277 | }, 278 | }) 279 | Expect(err).NotTo(HaveOccurred()) 280 | }) 281 | Expect(callCount).To(Equal(int64(1))) 282 | }) 283 | 284 | It("works without Value and error result", func() { 285 | var callCount int64 286 | perform(100, func(int) { 287 | err := mycache.Once(&cache.Item{ 288 | Ctx: ctx, 289 | Key: key, 290 | Do: func(*cache.Item) (interface{}, error) { 291 | time.Sleep(100 * time.Millisecond) 292 | atomic.AddInt64(&callCount, 1) 293 | return nil, errors.New("error stub") 294 | }, 295 | }) 296 | Expect(err).To(MatchError("error stub")) 297 | }) 298 | Expect(callCount).To(Equal(int64(1))) 299 | }) 300 | 301 | It("does not cache error result", func() { 302 | var callCount int64 303 | do := func(sleep time.Duration) (int, error) { 304 | var n int 305 | err := mycache.Once(&cache.Item{ 306 | Ctx: ctx, 307 | Key: key, 308 | Value: &n, 309 | Do: func(*cache.Item) (interface{}, error) { 310 | time.Sleep(sleep) 311 | 312 | n := atomic.AddInt64(&callCount, 1) 313 | if n == 1 { 314 | return nil, errors.New("error stub") 315 | } 316 | return 42, nil 317 | }, 318 | }) 319 | if err != nil { 320 | return 0, err 321 | } 322 | return n, nil 323 | } 324 | 325 | perform(100, func(int) { 326 | n, err := do(100 * time.Millisecond) 327 | Expect(err).To(MatchError("error stub")) 328 | Expect(n).To(Equal(0)) 329 | }) 330 | 331 | perform(100, func(int) { 332 | n, err := do(0) 333 | Expect(err).NotTo(HaveOccurred()) 334 | Expect(n).To(Equal(42)) 335 | }) 336 | 337 | Expect(callCount).To(Equal(int64(2))) 338 | }) 339 | 340 | It("skips Set when TTL = -1", func() { 341 | key := "skip-set" 342 | 343 | var value string 344 | err := mycache.Once(&cache.Item{ 345 | Ctx: ctx, 346 | Key: key, 347 | Value: &value, 348 | Do: func(item *cache.Item) (interface{}, error) { 349 | item.TTL = -1 350 | return "hello", nil 351 | }, 352 | }) 353 | Expect(err).NotTo(HaveOccurred()) 354 | Expect(value).To(Equal("hello")) 355 | 356 | if rdb != nil { 357 | exists, err := rdb.Exists(ctx, key).Result() 358 | Expect(err).NotTo(HaveOccurred()) 359 | Expect(exists).To(Equal(int64(0))) 360 | } 361 | }) 362 | }) 363 | } 364 | 365 | BeforeEach(func() { 366 | obj = &Object{ 367 | Str: "mystring", 368 | Num: 42, 369 | } 370 | }) 371 | 372 | Context("without LocalCache", func() { 373 | BeforeEach(func() { 374 | rdb = newRing() 375 | mycache = newCache(rdb) 376 | }) 377 | 378 | testCache() 379 | }) 380 | 381 | Context("with LocalCache", func() { 382 | BeforeEach(func() { 383 | rdb = newRing() 384 | mycache = newCacheWithLocal(rdb) 385 | }) 386 | 387 | testCache() 388 | }) 389 | 390 | Context("with LocalCache and without Redis", func() { 391 | BeforeEach(func() { 392 | rdb = nil 393 | mycache = cache.New(&cache.Options{ 394 | LocalCache: cache.NewTinyLFU(1000, time.Minute), 395 | }) 396 | }) 397 | 398 | testCache() 399 | }) 400 | }) 401 | 402 | func newRing() *redis.Ring { 403 | ctx := context.TODO() 404 | ring := redis.NewRing(&redis.RingOptions{ 405 | Addrs: map[string]string{ 406 | "server1": ":6379", 407 | }, 408 | }) 409 | _ = ring.ForEachShard(ctx, func(ctx context.Context, client *redis.Client) error { 410 | return client.FlushDB(ctx).Err() 411 | }) 412 | return ring 413 | } 414 | 415 | func newCache(rdb *redis.Ring) *cache.Cache { 416 | return cache.New(&cache.Options{ 417 | Redis: rdb, 418 | }) 419 | } 420 | 421 | func newCacheWithLocal(rdb *redis.Ring) *cache.Cache { 422 | return cache.New(&cache.Options{ 423 | Redis: rdb, 424 | LocalCache: cache.NewTinyLFU(1000, time.Minute), 425 | }) 426 | } 427 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 5 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 6 | github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= 7 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 8 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 13 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 14 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 15 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 16 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 17 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 18 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 19 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 20 | github.com/go-redis/redis/v8 v8.4.4 h1:fGqgxCTR1sydaKI00oQf3OmkU/DIe/I/fYXvGklCIuc= 21 | github.com/go-redis/redis/v8 v8.4.4/go.mod h1:nA0bQuF0i5JFx4Ta9RZxGKXFrQ8cRWntra97f0196iY= 22 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 23 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 24 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 25 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 26 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 27 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 28 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 29 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 30 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 31 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 32 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 33 | github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= 34 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 35 | github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA= 36 | github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 37 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 38 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 39 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 40 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 41 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 42 | github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= 43 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 44 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 45 | github.com/klauspost/compress v1.12.2 h1:2KCfW3I9M7nSc5wOqXAlW2v2U6v+w6cbjvbfp+OykW8= 46 | github.com/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= 47 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 48 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 49 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 50 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 51 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 52 | github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= 53 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 54 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 55 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 56 | github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M= 57 | github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 58 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 59 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 60 | github.com/onsi/gomega v1.10.4 h1:NiTx7EEvBzu9sFOD1zORteLSt3o8gnlvZZwSE9TnY9U= 61 | github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ= 62 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 63 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 64 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 65 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 66 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 67 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 68 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 69 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 70 | github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94= 71 | github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ= 72 | github.com/vmihailenco/go-tinylfu v0.2.1 h1:78/wH+STtgM8+fN2GdjvvKoxF3mkdzoOoKQTchQRj+g= 73 | github.com/vmihailenco/go-tinylfu v0.2.1/go.mod h1:CutYi2Q9puTxfcolkliPq4npPuofg9N9t8JVrjzwa3Q= 74 | github.com/vmihailenco/msgpack/v5 v5.1.0 h1:+od5YbEXxW95SPlW6beocmt8nOtlh83zqat5Ip9Hwdc= 75 | github.com/vmihailenco/msgpack/v5 v5.1.0/go.mod h1:C5gboKD0TJPqWDTVTtrQNfRbiBwHZGo8UTqP/9/XvLI= 76 | github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc= 77 | github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= 78 | go.opentelemetry.io/otel v0.15.0 h1:CZFy2lPhxd4HlhZnYK8gRyDotksO3Ip9rBweY1vVYJw= 79 | go.opentelemetry.io/otel v0.15.0/go.mod h1:e4GKElweB8W2gWUqbghw0B8t5MCTccc9212eNHnOHwA= 80 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 81 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 82 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 83 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 84 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 85 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= 86 | golang.org/x/exp v0.0.0-20201221025956-e89b829e73ea h1:GnGfrp0fiNhiBS/v/aCFTmfEWgkvxW4Qiu8oM2/IfZ4= 87 | golang.org/x/exp v0.0.0-20201221025956-e89b829e73ea/go.mod h1:I6l2HNBLBZEcrOoCpyKLdY2lHoRZ8lI4x60KMCQDft4= 88 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 89 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 90 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 91 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 92 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 93 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 94 | golang.org/x/mobile v0.0.0-20201217150744-e6ae53a27f4f/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= 95 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 96 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 97 | golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 98 | golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 99 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 100 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 101 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 102 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 103 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 104 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 105 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 106 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 107 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U= 108 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 109 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 110 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 111 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 112 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 113 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs= 114 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 115 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 116 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 117 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 118 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 119 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 120 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 121 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 122 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 123 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 124 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 125 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= 126 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 127 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 128 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 129 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 130 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 131 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 132 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 133 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 134 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 135 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 136 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 137 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 138 | golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 139 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 140 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 141 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 142 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 143 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 144 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 145 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 146 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 147 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 148 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 149 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 150 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 151 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 152 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 153 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 154 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 155 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 156 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 157 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 158 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 159 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 160 | google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= 161 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 162 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 163 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 164 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 165 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 166 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 167 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 168 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 169 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 170 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 171 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 172 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 173 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 174 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 175 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 176 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 177 | --------------------------------------------------------------------------------