├── go.mod ├── .gitignore ├── Makefile ├── .github └── workflows │ └── test.yml ├── _icons ├── coverage.svg ├── godoc.svg └── license.svg ├── cache_type_test.go ├── global.go ├── entry.go ├── _examples ├── load.go ├── fast_clock.go ├── ttl.go ├── sharding.go ├── task.go ├── basic.go ├── lru.go ├── lfu.go ├── gc.go ├── report.go └── performance_test.go ├── sharding_test.go ├── standard_test.go ├── load.go ├── config.go ├── cache_type.go ├── FUTURE.md ├── pkg ├── fastclock │ ├── fast_clock_test.go │ └── fast_clock.go ├── task │ ├── task.go │ └── task_test.go ├── singleflight │ ├── singleflight.go │ └── singleflight_test.go └── heap │ ├── heap.go │ └── heap_test.go ├── global_test.go ├── sharding.go ├── config_test.go ├── entry_test.go ├── load_test.go ├── lru_test.go ├── standard.go ├── HISTORY.md ├── lfu_test.go ├── cache.go ├── option.go ├── lfu.go ├── lru.go ├── report.go ├── README.md ├── README.en.md ├── option_test.go ├── report_test.go ├── cache_test.go └── LICENSE /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/FishGoddess/cachego 2 | 3 | go 1.21 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | .DS_Store 8 | 9 | # IDE 10 | .idea/ 11 | .vscode/ 12 | *.iml 13 | 14 | # Program 15 | target/ 16 | *.test 17 | *.out 18 | *.log 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test bench fmt 2 | 3 | all: test bench 4 | 5 | test: 6 | go test -cover -count=1 -test.cpu=1 ./... 7 | 8 | bench: 9 | go test -v ./_examples/performance_test.go -bench=. -benchtime=1s 10 | 11 | fmt: 12 | go fmt ./... -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test Project 2 | 3 | on: 4 | push: 5 | branches: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | test-project: 10 | runs-on: ubuntu-20.04 11 | steps: 12 | - name: Setup 13 | uses: actions/setup-go@v4 14 | with: 15 | go-version: "1.20" 16 | - run: go version 17 | 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Test 22 | run: make test 23 | -------------------------------------------------------------------------------- /_icons/coverage.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | coverage 12 | coverage 13 | 97% 14 | 97% 15 | 16 | -------------------------------------------------------------------------------- /_icons/godoc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | godoc 12 | godoc 13 | reference 14 | reference 15 | 16 | -------------------------------------------------------------------------------- /_icons/license.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | license 16 | license 17 | Apache 18 | Apache 19 | 20 | -------------------------------------------------------------------------------- /cache_type_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cachego 16 | 17 | import "testing" 18 | 19 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestCacheType$ 20 | func TestCacheType(t *testing.T) { 21 | if standard.String() != string(standard) { 22 | t.Fatalf("standard.String() %s is wrong", standard.String()) 23 | } 24 | 25 | if lru.String() != string(lru) { 26 | t.Fatalf("lru.String() %s is wrong", lru.String()) 27 | } 28 | 29 | if lfu.String() != string(lfu) { 30 | t.Fatalf("lfu.String() %s is wrong", lfu.String()) 31 | } 32 | 33 | if !standard.IsStandard() { 34 | t.Fatal("!standard.IsStandard()") 35 | } 36 | 37 | if !lru.IsLRU() { 38 | t.Fatal("!standard.IsLRU()") 39 | } 40 | 41 | if !lfu.IsLFU() { 42 | t.Fatal("!standard.IsLFU()") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /global.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cachego 16 | 17 | import "time" 18 | 19 | var ( 20 | mapInitialCap = 64 21 | sliceInitialCap = 64 22 | ) 23 | 24 | func hash(key string) int { 25 | hash := 1469598103934665603 26 | 27 | for _, r := range key { 28 | hash = (hash << 5) - hash + int(r&0xffff) 29 | hash *= 1099511628211 30 | } 31 | 32 | return hash 33 | } 34 | 35 | func now() int64 { 36 | return time.Now().UnixNano() 37 | } 38 | 39 | // SetMapInitialCap sets the initial capacity of map. 40 | func SetMapInitialCap(initialCap int) { 41 | if initialCap > 0 { 42 | mapInitialCap = initialCap 43 | } 44 | } 45 | 46 | // SetSliceInitialCap sets the initial capacity of slice. 47 | func SetSliceInitialCap(initialCap int) { 48 | if initialCap > 0 { 49 | sliceInitialCap = initialCap 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /entry.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cachego 16 | 17 | import "time" 18 | 19 | type entry struct { 20 | key string 21 | value interface{} 22 | 23 | // Time in nanosecond, valid util 2262 year (enough, right?) 24 | expiration int64 25 | now func() int64 26 | } 27 | 28 | func newEntry(key string, value interface{}, ttl time.Duration, now func() int64) *entry { 29 | e := &entry{ 30 | now: now, 31 | } 32 | 33 | e.setup(key, value, ttl) 34 | return e 35 | } 36 | 37 | func (e *entry) setup(key string, value interface{}, ttl time.Duration) { 38 | e.key = key 39 | e.value = value 40 | e.expiration = 0 41 | 42 | if ttl > 0 { 43 | e.expiration = e.now() + ttl.Nanoseconds() 44 | } 45 | } 46 | 47 | func (e *entry) expired(now int64) bool { 48 | if now > 0 { 49 | return e.expiration > 0 && e.expiration < now 50 | } 51 | 52 | return e.expiration > 0 && e.expiration < e.now() 53 | } 54 | -------------------------------------------------------------------------------- /_examples/load.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | "time" 20 | 21 | "github.com/FishGoddess/cachego" 22 | ) 23 | 24 | func main() { 25 | // By default, singleflight is enabled in cache. 26 | // Use WithDisableSingleflight to disable if you want. 27 | cache := cachego.NewCache(cachego.WithDisableSingleflight()) 28 | 29 | // We recommend you to use singleflight. 30 | cache = cachego.NewCache() 31 | 32 | value, ok := cache.Get("key") 33 | fmt.Println(value, ok) // false 34 | 35 | if !ok { 36 | // Load loads a value of key to cache with ttl. 37 | // Use cachego.NoTTL if you want this value is no ttl. 38 | // After loading value to cache, it returns the loaded value and error if failed. 39 | value, _ = cache.Load("key", time.Second, func() (value interface{}, err error) { 40 | return 666, nil 41 | }) 42 | } 43 | 44 | fmt.Println(value) // 666 45 | 46 | value, ok = cache.Get("key") 47 | fmt.Println(value, ok) // 666, true 48 | 49 | time.Sleep(2 * time.Second) 50 | 51 | value, ok = cache.Get("key") 52 | fmt.Println(value, ok) // , false 53 | } 54 | -------------------------------------------------------------------------------- /sharding_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cachego 16 | 17 | import ( 18 | "strconv" 19 | "testing" 20 | ) 21 | 22 | const ( 23 | testShardings = 4 24 | ) 25 | 26 | func newTestShardingCache() *shardingCache { 27 | conf := newDefaultConfig() 28 | conf.shardings = testShardings 29 | 30 | return newShardingCache(conf, newStandardCache).(*shardingCache) 31 | } 32 | 33 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestShardingCache$ 34 | func TestShardingCache(t *testing.T) { 35 | cache := newTestShardingCache() 36 | testCacheImplement(t, cache) 37 | } 38 | 39 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestShardingCacheIndex$ 40 | func TestShardingCacheIndex(t *testing.T) { 41 | cache := newTestShardingCache() 42 | 43 | if len(cache.caches) != testShardings { 44 | t.Fatalf("len(cache.caches) %d is wrong", len(cache.caches)) 45 | } 46 | 47 | for i := 0; i < 100; i++ { 48 | data := strconv.Itoa(i) 49 | cache.Set(data, data, NoTTL) 50 | } 51 | 52 | for i := range cache.caches { 53 | if cache.caches[i].Size() <= 0 { 54 | t.Fatalf("cache.caches[i].Size() %d <= 0", cache.caches[i].Size()) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /standard_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cachego 16 | 17 | import ( 18 | "strconv" 19 | "testing" 20 | "time" 21 | ) 22 | 23 | func newTestStandardCache() *standardCache { 24 | conf := newDefaultConfig() 25 | conf.maxEntries = maxTestEntries 26 | 27 | return newStandardCache(conf).(*standardCache) 28 | } 29 | 30 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestStandardCache$ 31 | func TestStandardCache(t *testing.T) { 32 | cache := newTestStandardCache() 33 | testCacheImplement(t, cache) 34 | } 35 | 36 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestStandardCacheEvict$ 37 | func TestStandardCacheEvict(t *testing.T) { 38 | cache := newTestStandardCache() 39 | 40 | for i := 0; i < cache.maxEntries*10; i++ { 41 | data := strconv.Itoa(i) 42 | evictedValue := cache.Set(data, data, time.Duration(i)*time.Second) 43 | 44 | if i >= cache.maxEntries && evictedValue == nil { 45 | t.Fatalf("i %d >= cache.maxEntries %d && evictedValue == nil", i, cache.maxEntries) 46 | } 47 | } 48 | 49 | if cache.Size() != cache.maxEntries { 50 | t.Fatalf("cache.Size() %d != cache.maxEntries %d", cache.Size(), cache.maxEntries) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /_examples/fast_clock.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | "math/rand" 20 | "time" 21 | 22 | "github.com/FishGoddess/cachego" 23 | "github.com/FishGoddess/cachego/pkg/fastclock" 24 | ) 25 | 26 | func main() { 27 | // Fast clock may return an "incorrect" time compared with time.Now. 28 | // The gap will be smaller than about 100 ms. 29 | for i := 0; i < 10; i++ { 30 | time.Sleep(time.Duration(rand.Int63n(int64(time.Second)))) 31 | 32 | timeNow := time.Now().UnixNano() 33 | clockNow := fastclock.NowNanos() 34 | 35 | fmt.Println(timeNow) 36 | fmt.Println(clockNow) 37 | fmt.Println("gap:", time.Duration(timeNow-clockNow)) 38 | fmt.Println() 39 | } 40 | 41 | // You can specify the fast clock to cache by WithNow. 42 | // All time used in this cache will be got from fast clock. 43 | cache := cachego.NewCache(cachego.WithNow(fastclock.NowNanos)) 44 | cache.Set("key", 666, 100*time.Millisecond) 45 | 46 | value, ok := cache.Get("key") 47 | fmt.Println(value, ok) // 666, true 48 | 49 | time.Sleep(200 * time.Millisecond) 50 | 51 | value, ok = cache.Get("key") 52 | fmt.Println(value, ok) // , false 53 | } 54 | -------------------------------------------------------------------------------- /load.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cachego 16 | 17 | import ( 18 | "errors" 19 | "time" 20 | 21 | flight "github.com/FishGoddess/cachego/pkg/singleflight" 22 | ) 23 | 24 | // loader loads values from somewhere. 25 | type loader struct { 26 | group *flight.Group 27 | } 28 | 29 | // newLoader creates a loader. 30 | // It also creates a singleflight group to call load if singleflight is true. 31 | func newLoader(singleflight bool) *loader { 32 | loader := new(loader) 33 | 34 | if singleflight { 35 | loader.group = flight.NewGroup(mapInitialCap) 36 | } 37 | 38 | return loader 39 | } 40 | 41 | // Load loads a value of key with ttl and returns an error if failed. 42 | func (l *loader) Load(key string, ttl time.Duration, load func() (value interface{}, err error)) (value interface{}, err error) { 43 | if load == nil { 44 | return nil, errors.New("cachego: load function is nil in loader") 45 | } 46 | 47 | if l.group == nil { 48 | return load() 49 | } 50 | 51 | return l.group.Call(key, load) 52 | } 53 | 54 | // Reset resets loader to initial status which is like a new loader. 55 | func (l *loader) Reset() { 56 | if l.group != nil { 57 | l.group.Reset() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cachego 16 | 17 | import "time" 18 | 19 | type config struct { 20 | cacheName string 21 | cacheType CacheType 22 | shardings int 23 | singleflight bool 24 | gcDuration time.Duration 25 | 26 | maxScans int 27 | maxEntries int 28 | 29 | now func() int64 30 | hash func(key string) int 31 | 32 | recordMissed bool 33 | recordHit bool 34 | recordGC bool 35 | recordLoad bool 36 | 37 | reportMissed func(reporter *Reporter, key string) 38 | reportHit func(reporter *Reporter, key string, value interface{}) 39 | reportGC func(reporter *Reporter, cost time.Duration, cleans int) 40 | reportLoad func(reporter *Reporter, key string, value interface{}, ttl time.Duration, err error) 41 | } 42 | 43 | func newDefaultConfig() *config { 44 | return &config{ 45 | cacheName: "", 46 | cacheType: standard, 47 | shardings: 0, 48 | singleflight: true, 49 | gcDuration: 10 * time.Minute, 50 | maxScans: 10000, 51 | maxEntries: 100000, 52 | now: now, 53 | hash: hash, 54 | recordMissed: true, 55 | recordHit: true, 56 | recordGC: true, 57 | recordLoad: true, 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /cache_type.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cachego 16 | 17 | const ( 18 | // standard cache is a simple cache with locked map. 19 | // It evicts entries randomly if cache size reaches to max entries. 20 | standard CacheType = "standard" 21 | 22 | // lru cache is a cache using lru to evict entries. 23 | // More details see https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU). 24 | lru CacheType = "lru" 25 | 26 | // lfu cache is a cache using lfu to evict entries. 27 | // More details see https://en.wikipedia.org/wiki/Cache_replacement_policies#Least-frequently_used_(LFU). 28 | lfu CacheType = "lfu" 29 | ) 30 | 31 | // CacheType is the type of cache. 32 | type CacheType string 33 | 34 | // String returns the cache type in string form. 35 | func (ct CacheType) String() string { 36 | return string(ct) 37 | } 38 | 39 | // IsStandard returns if cache type is standard. 40 | func (ct CacheType) IsStandard() bool { 41 | return ct == standard 42 | } 43 | 44 | // IsLRU returns if cache type is lru. 45 | func (ct CacheType) IsLRU() bool { 46 | return ct == lru 47 | } 48 | 49 | // IsLFU returns if cache type is lfu. 50 | func (ct CacheType) IsLFU() bool { 51 | return ct == lfu 52 | } 53 | -------------------------------------------------------------------------------- /_examples/ttl.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | "time" 20 | 21 | "github.com/FishGoddess/cachego" 22 | ) 23 | 24 | func main() { 25 | cache := cachego.NewCache() 26 | 27 | // We think most of the entries in cache should have its ttl. 28 | // So set an entry to cache should specify a ttl. 29 | cache.Set("key", 666, time.Second) 30 | 31 | value, ok := cache.Get("key") 32 | fmt.Println(value, ok) // 666 true 33 | 34 | time.Sleep(2 * time.Second) 35 | 36 | // The entry is expired after ttl. 37 | value, ok = cache.Get("key") 38 | fmt.Println(value, ok) // false 39 | 40 | // Notice that the entry still stores in cache even if it's expired. 41 | // This is because we think you will reset entry to cache after cache missing in most situations. 42 | // So we can reuse this entry and just reset its value and ttl. 43 | size := cache.Size() 44 | fmt.Println(size) // 1 45 | 46 | // What should I do if I want an expired entry never storing in cache? Try GC: 47 | cleans := cache.GC() 48 | fmt.Println(cleans) // 1 49 | 50 | size = cache.Size() 51 | fmt.Println(size) // 0 52 | 53 | // However, not all entries have ttl, and you can specify a NoTTL constant to do so. 54 | // In fact, the entry won't expire as long as its ttl is <= 0. 55 | // So you may have known NoTTL is a "readable" value of "<= 0". 56 | cache.Set("key", 666, cachego.NoTTL) 57 | } 58 | -------------------------------------------------------------------------------- /_examples/sharding.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | "strconv" 20 | 21 | "github.com/FishGoddess/cachego" 22 | ) 23 | 24 | func main() { 25 | // All operations in cache share one lock for concurrency. 26 | // Use read lock or write lock is depends on cache implements. 27 | // Get will use read lock in standard cache, but lru and lfu don't. 28 | // This may be a serious performance problem in high qps. 29 | cache := cachego.NewCache() 30 | 31 | // We provide a sharding cache wrapper to shard one cache to several parts with hash. 32 | // Every parts store its entries and all operations of one entry work on one part. 33 | // This means there are more than one lock when you operate entries. 34 | // The performance will be better in high qps. 35 | cache = cachego.NewCache(cachego.WithShardings(64)) 36 | cache.Set("key", 666, cachego.NoTTL) 37 | 38 | value, ok := cache.Get("key") 39 | fmt.Println(value, ok) // 666 true 40 | 41 | // Notice that max entries will be the sum of shards. 42 | // For example, we set WithShardings(4) and WithMaxEntries(100), and the max entries in whole cache will be 4 * 100. 43 | cache = cachego.NewCache(cachego.WithShardings(4), cachego.WithMaxEntries(100)) 44 | 45 | for i := 0; i < 1000; i++ { 46 | key := strconv.Itoa(i) 47 | cache.Set(key, i, cachego.NoTTL) 48 | } 49 | 50 | size := cache.Size() 51 | fmt.Println(size) // 400 52 | } 53 | -------------------------------------------------------------------------------- /FUTURE.md: -------------------------------------------------------------------------------- 1 | ## ✒ 未来版本的新特性 (Features in future versions) 2 | 3 | ### v1.0.0 4 | 5 | * [x] 稳定的 API 6 | 7 | ### v0.6.x 8 | 9 | * [x] 梳理代码,优化代码风格,精简部分代码和注释 10 | 11 | ### v0.5.x 12 | 13 | * [ ] ~~提供一个清空并设置全量值的方法,方便定时数据的全量替换~~ 14 | 目前还找不到一个合适的设计去加入这个功能,并且也不是非常刚需,通过业务手段可以处理,所以先不加 15 | 16 | ### v0.4.x 17 | 18 | * [x] 设计 Cache 接口,Get 方法用 bool 判断,单个锁结构 19 | * [x] 提供 ShardingCache 实现,实现 Cache 接口,细化锁粒度 20 | * [x] 提供多种接口实现,包括 standard,lru,lfu 等 21 | * [x] 提供 load 方法,集成 singleflight 进行数据加载 22 | * [x] 操作提供 option 机制,ttl 使用 option 设置,默认值使用 option 设置,最大遍历次数使用 option 设置 23 | * [x] Delete 方法改 Remove 并返回被删除的 value 24 | * [x] DeleteAll 方法改 Reset 25 | * [x] GC 方法保留,去除 AutoGC 方法 26 | * [x] 检查 pkg 代码,完善单元测试,提高覆盖率 27 | * [x] 清理废话注释,完善 examples 和性能测试 28 | * [x] 增加 report 机制用于监控缓存的情况 29 | * [x] 提取 now 和 hash 到缓存级别配置 30 | * [x] 提供定时缓存时间的机制,可选快速时钟 31 | * [x] 增加缓存名字配置,主要用于区分每个监控数据的来源 32 | * [x] 给 Reporter 增加缓存分片数量方法,主要用于监控缓存分片数量 33 | * [x] 给 Reporter 增加缓存类型方法,主要用于监控不同类型缓存的使用情况 34 | * [ ] ~~增加对不存在的数据做防穿透的机制~~ 35 | 经过实践,这个更适合业务方自己处理,所以这边就先去掉了 36 | 37 | ### v0.3.x 38 | 39 | * [ ] ~~支持内存大小限制,防止无上限的使用内存~~ 40 | * [ ] ~~支持用户自定义达到内存限制时的处理策略~~ 41 | * [ ] ~~支持缓存个数限制,防止数据量太多导致哈希性能下降~~ 42 | * [ ] ~~支持用户自定义达到个数限制时的处理策略~~ 43 | * [x] 去除 GetWithTTL 方法 44 | * [x] 重新设计 AutoSet 方法,引入 option 机制 45 | * [x] 加入 singleflight 机制 46 | * [ ] ~~加入 monitor 监控机制,接口形式~~ 47 | * [x] 优化 value 使用,复用内存、代码可读性 48 | * [ ] ~~GC 加入数量限制或时间限制~~ 49 | * [x] Set 引入 option 机制 50 | * [x] Get 引入 option 机制 51 | * [ ] ~~Delete 引入 option 机制,并可以限制删除数量、key 匹配模式~~ 52 | * [ ] ~~GC 引入 option 机制,并可以限制 GC 数量或时间、key 匹配模式~~ 53 | 54 | ### v0.2.x 55 | 56 | * [x] 创建缓存实例的方式需要改进 57 | * [ ] ~~性能优化 - 引入 value 实例池~~ 58 | 经过测试,这个实例池没有带来性能提升,反而影响了写入的性能,说明目前的性能瓶颈不在实例创建上,所以取消该特性。 59 | * [x] 增加 debug 网络调试点 60 | * [x] AutoSet 的构思,定时加载数据到缓存 61 | 62 | ### v0.1.0 63 | 64 | * [x] 简化设计,现在的我偏向于反设计 65 | * [x] 加入分段锁,使用更细粒度的锁机制保证更高的缓存性能 66 | * [x] 初步完善哨兵清理机制,配合分段锁,优化数据清理效率 67 | 68 | ### v0.0.1 69 | 70 | * [x] 简单实现一个并发访问安全、支持自动清理过期数据的缓存器 71 | * [x] 支持懒清理机制,每一次访问的时候判断是否过期 72 | * [x] 支持哨兵清理机制,每隔一定的时间间隔进行清理过期数据 73 | * [x] 基础特性和高级特性分离设计模式,减少新用户学习上手难度 74 | * [x] 链式编程友好的 API 设计,在一定程度上提供了很高的代码可读性 75 | -------------------------------------------------------------------------------- /_examples/task.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "time" 21 | 22 | "github.com/FishGoddess/cachego/pkg/task" 23 | ) 24 | 25 | var ( 26 | contextKey = struct{}{} 27 | ) 28 | 29 | func beforePrint(ctx context.Context) { 30 | fmt.Println("before:", ctx.Value(contextKey)) 31 | } 32 | 33 | func afterPrint(ctx context.Context) { 34 | fmt.Println("after:", ctx.Value(contextKey)) 35 | } 36 | 37 | func printContextValue(ctx context.Context) { 38 | fmt.Println("context value:", ctx.Value(contextKey)) 39 | } 40 | 41 | func main() { 42 | // Create a context to stop the task. 43 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 44 | defer cancel() 45 | 46 | // Wrap context with key and value 47 | ctx = context.WithValue(ctx, contextKey, "hello") 48 | 49 | // Use New to create a task and run it. 50 | // You can use it to load some hot data to cache at fixed duration. 51 | // Before is called before the task loop, optional. 52 | // After is called after the task loop, optional. 53 | // Context is passed to fn include fn/before/after which can stop the task by Done(), optional. 54 | // Duration is the duration between two loop of fn, optional. 55 | // Run will start a new goroutine and run the task loop. 56 | // The task will stop if context is done. 57 | task.New(printContextValue).Before(beforePrint).After(afterPrint).Context(ctx).Duration(time.Second).Run() 58 | } 59 | -------------------------------------------------------------------------------- /pkg/fastclock/fast_clock_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package fastclock 16 | 17 | import ( 18 | "math" 19 | "math/rand" 20 | "testing" 21 | "time" 22 | ) 23 | 24 | // go test -v -run=^$ -bench=^BenchmarkTimeNow$ -benchtime=1s 25 | func BenchmarkTimeNow(b *testing.B) { 26 | b.ReportAllocs() 27 | b.ResetTimer() 28 | 29 | for i := 0; i < b.N; i++ { 30 | time.Now() 31 | } 32 | } 33 | 34 | // go test -v -run=^$ -bench=^BenchmarkFastClockNow$ -benchtime=1s 35 | func BenchmarkFastClockNow(b *testing.B) { 36 | b.ReportAllocs() 37 | b.ResetTimer() 38 | 39 | for i := 0; i < b.N; i++ { 40 | Now() 41 | } 42 | } 43 | 44 | // go test -v -run=^$ -bench=^BenchmarkFastClockNowNanos$ -benchtime=1s 45 | func BenchmarkFastClockNowNanos(b *testing.B) { 46 | b.ReportAllocs() 47 | b.ResetTimer() 48 | 49 | for i := 0; i < b.N; i++ { 50 | NowNanos() 51 | } 52 | } 53 | 54 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestNow$ 55 | func TestNow(t *testing.T) { 56 | duration := 100 * time.Millisecond 57 | 58 | for i := 0; i < 100; i++ { 59 | got := Now() 60 | gap := time.Since(got) 61 | t.Logf("got: %v, gap: %v", got, gap) 62 | 63 | if math.Abs(float64(gap.Nanoseconds())) > float64(duration)*1.1 { 64 | t.Errorf("now %v is wrong", got) 65 | } 66 | 67 | time.Sleep(time.Duration(rand.Int63n(int64(duration)))) 68 | } 69 | } 70 | 71 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestNowNanos$ 72 | func TestNowNanos(t *testing.T) { 73 | duration := 100 * time.Millisecond 74 | 75 | for i := 0; i < 100; i++ { 76 | gotNanos := NowNanos() 77 | got := time.Unix(0, gotNanos) 78 | gap := time.Since(got) 79 | t.Logf("got: %v, gap: %v", got, gap) 80 | 81 | if math.Abs(float64(gap.Nanoseconds())) > float64(duration)*1.1 { 82 | t.Errorf("now %v is wrong", got) 83 | } 84 | 85 | time.Sleep(time.Duration(rand.Int63n(int64(duration)))) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /pkg/task/task.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package task 16 | 17 | import ( 18 | "context" 19 | "time" 20 | ) 21 | 22 | // Task runs a function at fixed duration. 23 | type Task struct { 24 | ctx context.Context 25 | duration time.Duration 26 | 27 | before func(ctx context.Context) 28 | fn func(ctx context.Context) 29 | after func(ctx context.Context) 30 | } 31 | 32 | // New returns a new task for use. 33 | // fn is main function which will called in task loop. 34 | // By default, its duration is 1min, and you can change it by Duration(). 35 | func New(fn func(ctx context.Context)) *Task { 36 | return &Task{ 37 | ctx: context.Background(), 38 | duration: time.Minute, 39 | fn: fn, 40 | } 41 | } 42 | 43 | // Context sets ctx to task which will be passed to its functions in order to control context. 44 | func (t *Task) Context(ctx context.Context) *Task { 45 | t.ctx = ctx 46 | return t 47 | } 48 | 49 | // Duration sets duration to task which controls the duration between two task loops. 50 | func (t *Task) Duration(duration time.Duration) *Task { 51 | t.duration = duration 52 | return t 53 | } 54 | 55 | // Before sets fn to task which will be called before task starting. 56 | func (t *Task) Before(fn func(ctx context.Context)) *Task { 57 | t.before = fn 58 | return t 59 | } 60 | 61 | // After sets fn to task which will be called after task stopping. 62 | func (t *Task) After(fn func(ctx context.Context)) *Task { 63 | t.after = fn 64 | return t 65 | } 66 | 67 | // Run runs task. 68 | // You can use context to stop this task, see context.Context. 69 | func (t *Task) Run() { 70 | if t.fn == nil { 71 | return 72 | } 73 | 74 | if t.before != nil { 75 | t.before(t.ctx) 76 | } 77 | 78 | if t.after != nil { 79 | defer t.after(t.ctx) 80 | } 81 | 82 | ticker := time.NewTicker(t.duration) 83 | defer ticker.Stop() 84 | 85 | for { 86 | select { 87 | case <-t.ctx.Done(): 88 | return 89 | case <-ticker.C: 90 | t.fn(t.ctx) 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /global_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cachego 16 | 17 | import ( 18 | "testing" 19 | "time" 20 | ) 21 | 22 | // go test -v -bench=^BenchmarkHash$ -benchtime=1s ./global.go ./global_test.go 23 | func BenchmarkHash(b *testing.B) { 24 | b.ReportAllocs() 25 | b.ResetTimer() 26 | 27 | for i := 0; i < b.N; i++ { 28 | hash("key") 29 | } 30 | } 31 | 32 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestHash$ 33 | func TestHash(t *testing.T) { 34 | hash := hash("test") 35 | if hash < 0 { 36 | t.Fatalf("hash %d <= 0", hash) 37 | } 38 | } 39 | 40 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestNow$ 41 | func TestNow(t *testing.T) { 42 | got := now() 43 | expect := time.Now().UnixNano() 44 | 45 | if got > expect || got < expect-testDurationGap.Nanoseconds() { 46 | t.Fatalf("got %d != expect %d", got, expect) 47 | } 48 | } 49 | 50 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestSetMapInitialCap$ 51 | func TestSetMapInitialCap(t *testing.T) { 52 | oldInitialCap := mapInitialCap 53 | 54 | SetMapInitialCap(-2) 55 | if mapInitialCap != oldInitialCap { 56 | t.Fatalf("mapInitialCap %d is wrong", mapInitialCap) 57 | } 58 | 59 | SetMapInitialCap(0) 60 | if mapInitialCap != oldInitialCap { 61 | t.Fatalf("mapInitialCap %d is wrong", mapInitialCap) 62 | } 63 | 64 | SetMapInitialCap(2) 65 | if mapInitialCap != 2 { 66 | t.Fatalf("mapInitialCap %d is wrong", mapInitialCap) 67 | } 68 | } 69 | 70 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestSetSliceInitialCap$ 71 | func TestSetSliceInitialCap(t *testing.T) { 72 | oldInitialCap := sliceInitialCap 73 | 74 | SetSliceInitialCap(-2) 75 | if sliceInitialCap != oldInitialCap { 76 | t.Fatalf("sliceInitialCap %d is wrong", sliceInitialCap) 77 | } 78 | 79 | SetSliceInitialCap(0) 80 | if sliceInitialCap != oldInitialCap { 81 | t.Fatalf("sliceInitialCap %d is wrong", sliceInitialCap) 82 | } 83 | 84 | SetSliceInitialCap(2) 85 | if sliceInitialCap != 2 { 86 | t.Fatalf("sliceInitialCap %d is wrong", sliceInitialCap) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /_examples/basic.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | "time" 20 | 21 | "github.com/FishGoddess/cachego" 22 | ) 23 | 24 | func main() { 25 | // Use NewCache function to create a cache. 26 | // By default, it creates a standard cache which evicts entries randomly. 27 | // Use WithShardings to shard cache to several parts for higher performance. 28 | // Use WithGC to clean expired entries every 10 minutes. 29 | cache := cachego.NewCache(cachego.WithGC(10*time.Minute), cachego.WithShardings(64)) 30 | 31 | // Set an entry to cache with ttl. 32 | cache.Set("key", 123, time.Second) 33 | 34 | // Get an entry from cache. 35 | value, ok := cache.Get("key") 36 | fmt.Println(value, ok) // 123 true 37 | 38 | // Check how many entries stores in cache. 39 | size := cache.Size() 40 | fmt.Println(size) // 1 41 | 42 | // Clean expired entries. 43 | cleans := cache.GC() 44 | fmt.Println(cleans) // 1 45 | 46 | // Set an entry which doesn't have ttl. 47 | cache.Set("key", 123, cachego.NoTTL) 48 | 49 | // Remove an entry. 50 | removedValue := cache.Remove("key") 51 | fmt.Println(removedValue) // 123 52 | 53 | // Reset resets cache to initial status. 54 | cache.Reset() 55 | 56 | // Get value from cache and load it to cache if not found. 57 | value, ok = cache.Get("key") 58 | if !ok { 59 | // Loaded entry will be set to cache and returned. 60 | // By default, it will use singleflight. 61 | value, _ = cache.Load("key", time.Second, func() (value interface{}, err error) { 62 | return 666, nil 63 | }) 64 | } 65 | 66 | fmt.Println(value) // 666 67 | 68 | // You can use WithLRU to specify the type of cache to lru. 69 | // Also, try WithLFU if you want to use lfu to evict data. 70 | cache = cachego.NewCache(cachego.WithLRU(100)) 71 | cache = cachego.NewCache(cachego.WithLFU(100)) 72 | 73 | // Use NewCacheWithReport to create a cache with report. 74 | cache, reporter := cachego.NewCacheWithReport(cachego.WithCacheName("test")) 75 | fmt.Println(reporter.CacheName()) 76 | fmt.Println(reporter.CacheType()) 77 | } 78 | -------------------------------------------------------------------------------- /pkg/fastclock/fast_clock.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package fastclock 16 | 17 | import ( 18 | "sync" 19 | "sync/atomic" 20 | "time" 21 | ) 22 | 23 | // fastClock is a clock for getting current time faster. 24 | // It caches current time in nanos and updates it in fixed duration, so it's not a precise way to get current time. 25 | // In fact, we don't recommend you to use it unless you do need a fast way to get current time even the time is "incorrect". 26 | // According to our benchmarks, it does run faster than time.Now: 27 | // 28 | // In my linux server with 2 cores: 29 | // BenchmarkTimeNow-2 19150246 62.26 ns/op 0 B/op 0 allocs/op 30 | // BenchmarkFastClockNow-2 357209233 3.46 ns/op 0 B/op 0 allocs/op 31 | // BenchmarkFastClockNowNanos-2 467461363 2.55 ns/op 0 B/op 0 allocs/op 32 | // 33 | // However, the performance of time.Now is faster enough for 99.9% situations, so we hope you never use it :) 34 | type fastClock struct { 35 | nanos int64 36 | } 37 | 38 | func newClock() *fastClock { 39 | clock := &fastClock{ 40 | nanos: time.Now().UnixNano(), 41 | } 42 | 43 | go clock.start() 44 | return clock 45 | } 46 | 47 | func (fc *fastClock) start() { 48 | const duration = 100 * time.Millisecond 49 | 50 | for { 51 | for i := 0; i < 9; i++ { 52 | time.Sleep(duration) 53 | atomic.AddInt64(&fc.nanos, int64(duration)) 54 | } 55 | 56 | time.Sleep(duration) 57 | atomic.StoreInt64(&fc.nanos, time.Now().UnixNano()) 58 | } 59 | } 60 | 61 | func (fc *fastClock) currentNanos() int64 { 62 | return atomic.LoadInt64(&fc.nanos) 63 | } 64 | 65 | var ( 66 | clock *fastClock 67 | clockOnce sync.Once 68 | ) 69 | 70 | // Now returns the current time from fast clock. 71 | func Now() time.Time { 72 | nanos := NowNanos() 73 | return time.Unix(0, nanos) 74 | } 75 | 76 | // NowNanos returns the current time in nanos from fast clock. 77 | func NowNanos() int64 { 78 | clockOnce.Do(func() { 79 | clock = newClock() 80 | }) 81 | 82 | return clock.currentNanos() 83 | } 84 | -------------------------------------------------------------------------------- /_examples/lru.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | "strconv" 20 | 21 | "github.com/FishGoddess/cachego" 22 | ) 23 | 24 | func main() { 25 | // By default, NewCache() returns a standard cache which evicts entries randomly. 26 | cache := cachego.NewCache(cachego.WithMaxEntries(10)) 27 | 28 | for i := 0; i < 20; i++ { 29 | key := strconv.Itoa(i) 30 | cache.Set(key, i, cachego.NoTTL) 31 | } 32 | 33 | // Since we set 20 entries to cache, the size won't be 20 because we limit the max entries to 10. 34 | size := cache.Size() 35 | fmt.Println(size) // 10 36 | 37 | // We don't know which entries will be evicted and stayed. 38 | for i := 0; i < 20; i++ { 39 | key := strconv.Itoa(i) 40 | value, ok := cache.Get(key) 41 | fmt.Println(key, value, ok) 42 | } 43 | 44 | fmt.Println() 45 | 46 | // Sometimes we want it evicts entries by lru, try WithLRU. 47 | // You need to specify the max entries storing in lru cache. 48 | // More details see https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU). 49 | cache = cachego.NewCache(cachego.WithLRU(10)) 50 | 51 | for i := 0; i < 20; i++ { 52 | key := strconv.Itoa(i) 53 | cache.Set(key, i, cachego.NoTTL) 54 | } 55 | 56 | // Only the least recently used entries can be got in a lru cache. 57 | for i := 0; i < 20; i++ { 58 | key := strconv.Itoa(i) 59 | value, ok := cache.Get(key) 60 | fmt.Println(key, value, ok) 61 | } 62 | 63 | // By default, lru will share one lock to do all operations. 64 | // You can sharding cache to several parts for higher performance. 65 | // Notice that max entries only effect to one part in sharding mode. 66 | // For example, the total max entries will be 2*10 if shardings is 2 and max entries is 10 in WithLRU or WithMaxEntries. 67 | // In some cache libraries, they will calculate max entries in each parts of shardings, like 10/2. 68 | // However, the result divided by max entries and shardings may be not an integer which will make the total max entries incorrect. 69 | // So we let users decide the exact max entries in each parts of shardings. 70 | cache = cachego.NewCache(cachego.WithShardings(2), cachego.WithLRU(10)) 71 | } 72 | -------------------------------------------------------------------------------- /pkg/singleflight/singleflight.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package singleflight 16 | 17 | import ( 18 | "sync" 19 | ) 20 | 21 | type call struct { 22 | fn func() (result interface{}, err error) 23 | result interface{} 24 | err error 25 | 26 | // deleted is a flag checking if this call has been deleted from Group. 27 | deleted bool 28 | 29 | wg sync.WaitGroup 30 | } 31 | 32 | func newCall(fn func() (result interface{}, err error)) *call { 33 | return &call{ 34 | fn: fn, 35 | deleted: false, 36 | } 37 | } 38 | 39 | func (c *call) do() { 40 | defer c.wg.Done() 41 | 42 | // Notice: Any panics or runtime.Goexit() happening in fn() will be ignored. 43 | c.result, c.err = c.fn() 44 | } 45 | 46 | // Group stores all function calls in it. 47 | type Group struct { 48 | calls map[string]*call 49 | lock sync.Mutex 50 | } 51 | 52 | // NewGroup returns a new Group with initialCap. 53 | func NewGroup(initialCap int) *Group { 54 | return &Group{ 55 | calls: make(map[string]*call, initialCap), 56 | } 57 | } 58 | 59 | // Call calls fn in singleflight mode and returns its result and error. 60 | func (g *Group) Call(key string, fn func() (interface{}, error)) (interface{}, error) { 61 | g.lock.Lock() 62 | 63 | if c, ok := g.calls[key]; ok { 64 | g.lock.Unlock() 65 | 66 | c.wg.Wait() 67 | return c.result, c.err 68 | } 69 | 70 | c := newCall(fn) 71 | c.wg.Add(1) 72 | 73 | g.calls[key] = c 74 | g.lock.Unlock() 75 | 76 | c.do() 77 | g.lock.Lock() 78 | 79 | if !c.deleted { 80 | delete(g.calls, key) 81 | } 82 | 83 | g.lock.Unlock() 84 | return c.result, c.err 85 | } 86 | 87 | // Delete deletes the call of key so a new call can be called. 88 | func (g *Group) Delete(key string) { 89 | g.lock.Lock() 90 | defer g.lock.Unlock() 91 | 92 | if c, ok := g.calls[key]; ok { 93 | delete(g.calls, key) 94 | c.deleted = true 95 | } 96 | } 97 | 98 | // Reset resets group to initial status. 99 | func (g *Group) Reset() { 100 | g.lock.Lock() 101 | defer g.lock.Unlock() 102 | 103 | for key, c := range g.calls { 104 | delete(g.calls, key) 105 | c.deleted = true 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /_examples/lfu.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | "strconv" 20 | 21 | "github.com/FishGoddess/cachego" 22 | ) 23 | 24 | func main() { 25 | // By default, NewCache() returns a standard cache which evicts entries randomly. 26 | cache := cachego.NewCache(cachego.WithMaxEntries(10)) 27 | 28 | for i := 0; i < 20; i++ { 29 | key := strconv.Itoa(i) 30 | cache.Set(key, i, cachego.NoTTL) 31 | } 32 | 33 | // Since we set 20 entries to cache, the size won't be 20 because we limit the max entries to 10. 34 | size := cache.Size() 35 | fmt.Println(size) // 10 36 | 37 | // We don't know which entries will be evicted and stayed. 38 | for i := 0; i < 20; i++ { 39 | key := strconv.Itoa(i) 40 | value, ok := cache.Get(key) 41 | fmt.Println(key, value, ok) 42 | } 43 | 44 | fmt.Println() 45 | 46 | // Sometimes we want it evicts entries by lfu, try WithLFU. 47 | // You need to specify the max entries storing in lfu cache. 48 | // More details see https://en.wikipedia.org/wiki/Cache_replacement_policies#Least-frequently_used_(LFU). 49 | cache = cachego.NewCache(cachego.WithLFU(10)) 50 | 51 | for i := 0; i < 20; i++ { 52 | key := strconv.Itoa(i) 53 | 54 | // Let entries have some frequently used operations. 55 | for j := 0; j < i; j++ { 56 | cache.Set(key, i, cachego.NoTTL) 57 | } 58 | } 59 | 60 | for i := 0; i < 20; i++ { 61 | key := strconv.Itoa(i) 62 | value, ok := cache.Get(key) 63 | fmt.Println(key, value, ok) 64 | } 65 | 66 | // By default, lfu will share one lock to do all operations. 67 | // You can sharding cache to several parts for higher performance. 68 | // Notice that max entries only effect to one part in sharding mode. 69 | // For example, the total max entries will be 2*10 if shardings is 2 and max entries is 10 in WithLFU or WithMaxEntries. 70 | // In some cache libraries, they will calculate max entries in each parts of shardings, like 10/2. 71 | // However, the result divided by max entries and shardings may be not an integer which will make the total max entries incorrect. 72 | // So we let users decide the exact max entries in each parts of shardings. 73 | cache = cachego.NewCache(cachego.WithShardings(2), cachego.WithLFU(10)) 74 | } 75 | -------------------------------------------------------------------------------- /_examples/gc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | "time" 20 | 21 | "github.com/FishGoddess/cachego" 22 | ) 23 | 24 | func main() { 25 | cache := cachego.NewCache() 26 | cache.Set("key", 666, time.Second) 27 | 28 | time.Sleep(2 * time.Second) 29 | 30 | // The entry is expired after ttl. 31 | value, ok := cache.Get("key") 32 | fmt.Println(value, ok) // false 33 | 34 | // As you know the entry still stores in cache even if it's expired. 35 | // This is because we think you will reset entry to cache after cache missing in most situations. 36 | // So we can reuse this entry and just reset its value and ttl. 37 | size := cache.Size() 38 | fmt.Println(size) // 1 39 | 40 | // What should I do if I want an expired entry never storing in cache? Try GC: 41 | cleans := cache.GC() 42 | fmt.Println(cleans) // 1 43 | 44 | // Is there a smart way to do that? Try WithGC: 45 | // For testing, we set a small duration of gc. 46 | // You should set at least 3 minutes in production for performance. 47 | cache = cachego.NewCache(cachego.WithGC(2 * time.Second)) 48 | cache.Set("key", 666, time.Second) 49 | 50 | size = cache.Size() 51 | fmt.Println(size) // 1 52 | 53 | time.Sleep(3 * time.Second) 54 | 55 | size = cache.Size() 56 | fmt.Println(size) // 0 57 | 58 | // Or you want a cancalable gc task? Try RunGCTask: 59 | cache = cachego.NewCache() 60 | cancel := cachego.RunGCTask(cache, 2*time.Second) 61 | 62 | cache.Set("key", 666, time.Second) 63 | 64 | size = cache.Size() 65 | fmt.Println(size) // 1 66 | 67 | time.Sleep(3 * time.Second) 68 | 69 | size = cache.Size() 70 | fmt.Println(size) // 0 71 | 72 | cancel() 73 | 74 | cache.Set("key", 666, time.Second) 75 | 76 | size = cache.Size() 77 | fmt.Println(size) // 1 78 | 79 | time.Sleep(3 * time.Second) 80 | 81 | size = cache.Size() 82 | fmt.Println(size) // 1 83 | 84 | // By default, gc only scans at most maxScans entries one time to remove expired entries. 85 | // This is because scans all entries may cost much time if there is so many entries in cache, and a "stw" will happen. 86 | // This can be a serious problem in some situations. 87 | // Use WithMaxScans to set this value, remember, a value <= 0 means no scan limit. 88 | cache = cachego.NewCache(cachego.WithGC(10*time.Minute), cachego.WithMaxScans(0)) 89 | } 90 | -------------------------------------------------------------------------------- /pkg/task/task_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package task 16 | 17 | import ( 18 | "context" 19 | "strings" 20 | "sync/atomic" 21 | "testing" 22 | "time" 23 | ) 24 | 25 | type testEntry struct { 26 | key string 27 | value string 28 | } 29 | 30 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestTickerTaskRun$ 31 | func TestTickerTaskRun(t *testing.T) { 32 | before := testEntry{key: "before_key", value: "before_value"} 33 | fn := testEntry{key: "task_key", value: "task_value"} 34 | after := testEntry{key: "after_key", value: "after_value"} 35 | 36 | var loop int64 37 | var result strings.Builder 38 | 39 | beforeFn := func(ctx context.Context) { 40 | value, ok := ctx.Value(before.key).(string) 41 | if !ok { 42 | t.Fatalf("ctx.Value(before.key).(string) %+v failed", ctx.Value(before.key)) 43 | } 44 | 45 | if value != before.value { 46 | t.Fatalf("value %s != before.value %s", value, before.value) 47 | } 48 | 49 | result.WriteString(value) 50 | } 51 | 52 | mainFn := func(ctx context.Context) { 53 | value, ok := ctx.Value(fn.key).(string) 54 | if !ok { 55 | t.Fatalf("ctx.Value(fn.key).(string) %+v failed", ctx.Value(fn.key)) 56 | } 57 | 58 | if value != fn.value { 59 | t.Fatalf("value %s != fn.value %s", value, fn.value) 60 | } 61 | 62 | atomic.AddInt64(&loop, 1) 63 | result.WriteString(value) 64 | } 65 | 66 | afterFn := func(ctx context.Context) { 67 | value, ok := ctx.Value(after.key).(string) 68 | if !ok { 69 | t.Fatalf("ctx.Value(after.key).(string) %+v failed", ctx.Value(after.key)) 70 | } 71 | 72 | if value != after.value { 73 | t.Fatalf("value %s != after.value %s", value, after.value) 74 | } 75 | 76 | result.WriteString(value) 77 | } 78 | 79 | ctx := context.WithValue(context.Background(), before.key, before.value) 80 | ctx = context.WithValue(ctx, fn.key, fn.value) 81 | ctx = context.WithValue(ctx, after.key, after.value) 82 | 83 | ctx, cancel := context.WithTimeout(ctx, 10*time.Millisecond) 84 | defer cancel() 85 | 86 | go New(mainFn).Context(ctx).Before(beforeFn).After(afterFn).Duration(3 * time.Millisecond).Run() 87 | time.Sleep(time.Second) 88 | 89 | var expect strings.Builder 90 | expect.WriteString(before.value) 91 | 92 | for i := int64(0); i < atomic.LoadInt64(&loop); i++ { 93 | expect.WriteString(fn.value) 94 | } 95 | 96 | expect.WriteString(after.value) 97 | 98 | if result.String() != expect.String() { 99 | t.Fatalf("result %s != expect %s", result.String(), expect.String()) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /sharding.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cachego 16 | 17 | import ( 18 | "math/bits" 19 | "time" 20 | ) 21 | 22 | type shardingCache struct { 23 | *config 24 | caches []Cache 25 | } 26 | 27 | func newShardingCache(conf *config, newCache func(conf *config) Cache) Cache { 28 | if conf.shardings <= 0 { 29 | panic("cachego: shardings must be > 0.") 30 | } 31 | 32 | if bits.OnesCount(uint(conf.shardings)) > 1 { 33 | panic("cachego: shardings must be the pow of 2 (such as 64).") 34 | } 35 | 36 | caches := make([]Cache, 0, conf.shardings) 37 | for i := 0; i < conf.shardings; i++ { 38 | caches = append(caches, newCache(conf)) 39 | } 40 | 41 | cache := &shardingCache{ 42 | config: conf, 43 | caches: caches, 44 | } 45 | 46 | return cache 47 | } 48 | 49 | func (sc *shardingCache) cacheOf(key string) Cache { 50 | hash := sc.hash(key) 51 | mask := len(sc.caches) - 1 52 | 53 | return sc.caches[hash&mask] 54 | } 55 | 56 | // Get gets the value of key from cache and returns value if found. 57 | func (sc *shardingCache) Get(key string) (value interface{}, found bool) { 58 | return sc.cacheOf(key).Get(key) 59 | } 60 | 61 | // Set sets key and value to cache with ttl and returns evicted value if exists and unexpired. 62 | // See Cache interface. 63 | func (sc *shardingCache) Set(key string, value interface{}, ttl time.Duration) (oldValue interface{}) { 64 | return sc.cacheOf(key).Set(key, value, ttl) 65 | } 66 | 67 | // Remove removes key and returns the removed value of key. 68 | // See Cache interface. 69 | func (sc *shardingCache) Remove(key string) (removedValue interface{}) { 70 | return sc.cacheOf(key).Remove(key) 71 | } 72 | 73 | // Size returns the count of keys in cache. 74 | // See Cache interface. 75 | func (sc *shardingCache) Size() (size int) { 76 | for _, cache := range sc.caches { 77 | size += cache.Size() 78 | } 79 | 80 | return size 81 | } 82 | 83 | // GC cleans the expired keys in cache and returns the exact count cleaned. 84 | // See Cache interface. 85 | func (sc *shardingCache) GC() (cleans int) { 86 | for _, cache := range sc.caches { 87 | cleans += cache.GC() 88 | } 89 | 90 | return cleans 91 | } 92 | 93 | // Reset resets cache to initial status which is like a new cache. 94 | // See Cache interface. 95 | func (sc *shardingCache) Reset() { 96 | for _, cache := range sc.caches { 97 | cache.Reset() 98 | } 99 | } 100 | 101 | // Load loads a value by load function and sets it to cache. 102 | // Returns an error if load failed. 103 | func (sc *shardingCache) Load(key string, ttl time.Duration, load func() (value interface{}, err error)) (value interface{}, err error) { 104 | return sc.cacheOf(key).Load(key, ttl, load) 105 | } 106 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cachego 16 | 17 | import ( 18 | "fmt" 19 | "testing" 20 | ) 21 | 22 | func isConfigEquals(conf1 *config, conf2 *config) bool { 23 | if conf1.cacheType != conf2.cacheType { 24 | return false 25 | } 26 | 27 | if conf1.shardings != conf2.shardings { 28 | return false 29 | } 30 | 31 | if conf1.singleflight != conf2.singleflight { 32 | return false 33 | } 34 | 35 | if conf1.gcDuration != conf2.gcDuration { 36 | return false 37 | } 38 | 39 | if conf1.maxScans != conf2.maxScans { 40 | return false 41 | } 42 | 43 | if conf1.maxEntries != conf2.maxEntries { 44 | return false 45 | } 46 | 47 | if fmt.Sprintf("%p", conf1.now) != fmt.Sprintf("%p", conf2.now) { 48 | return false 49 | } 50 | 51 | if fmt.Sprintf("%p", conf1.hash) != fmt.Sprintf("%p", conf2.hash) { 52 | return false 53 | } 54 | 55 | if conf1.recordMissed != conf2.recordMissed { 56 | return false 57 | } 58 | 59 | if conf1.recordHit != conf2.recordHit { 60 | return false 61 | } 62 | 63 | if conf1.recordGC != conf2.recordGC { 64 | return false 65 | } 66 | 67 | if conf1.recordLoad != conf2.recordLoad { 68 | return false 69 | } 70 | 71 | if fmt.Sprintf("%p", conf1.reportMissed) != fmt.Sprintf("%p", conf2.reportMissed) { 72 | return false 73 | } 74 | 75 | if fmt.Sprintf("%p", conf1.reportHit) != fmt.Sprintf("%p", conf2.reportHit) { 76 | return false 77 | } 78 | 79 | if fmt.Sprintf("%p", conf1.reportGC) != fmt.Sprintf("%p", conf2.reportGC) { 80 | return false 81 | } 82 | 83 | if fmt.Sprintf("%p", conf1.reportLoad) != fmt.Sprintf("%p", conf2.reportLoad) { 84 | return false 85 | } 86 | 87 | return true 88 | } 89 | 90 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestApplyOptions$ 91 | func TestApplyOptions(t *testing.T) { 92 | got := &config{ 93 | shardings: 0, 94 | singleflight: true, 95 | gcDuration: 0, 96 | maxScans: 0, 97 | maxEntries: 0, 98 | recordMissed: false, 99 | recordHit: false, 100 | recordGC: false, 101 | recordLoad: false, 102 | } 103 | 104 | expect := &config{ 105 | shardings: 1, 106 | singleflight: false, 107 | gcDuration: 2, 108 | maxScans: 3, 109 | maxEntries: 4, 110 | recordMissed: true, 111 | recordHit: true, 112 | recordGC: true, 113 | recordLoad: true, 114 | } 115 | 116 | applyOptions(got, []Option{ 117 | WithShardings(1), 118 | WithDisableSingleflight(), 119 | WithGC(2), 120 | WithMaxScans(3), 121 | WithMaxEntries(4), 122 | WithRecordMissed(true), 123 | WithRecordHit(true), 124 | WithRecordGC(true), 125 | WithRecordLoad(true), 126 | }) 127 | 128 | if !isConfigEquals(got, expect) { 129 | t.Fatalf("got %+v != expect %+v", got, expect) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /_examples/report.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | "io" 20 | "strconv" 21 | "time" 22 | 23 | "github.com/FishGoddess/cachego" 24 | ) 25 | 26 | func reportMissed(reporter *cachego.Reporter, key string) { 27 | fmt.Printf("report: missed key %s, missed rate %.3f\n", key, reporter.MissedRate()) 28 | } 29 | 30 | func reportHit(reporter *cachego.Reporter, key string, value interface{}) { 31 | fmt.Printf("report: hit key %s value %+v, hit rate %.3f\n", key, value, reporter.HitRate()) 32 | } 33 | 34 | func reportGC(reporter *cachego.Reporter, cost time.Duration, cleans int) { 35 | fmt.Printf("report: gc cost %s cleans %d, gc count %d, cache size %d\n", cost, cleans, reporter.CountGC(), reporter.CacheSize()) 36 | } 37 | 38 | func reportLoad(reporter *cachego.Reporter, key string, value interface{}, ttl time.Duration, err error) { 39 | fmt.Printf("report: load key %s value %+v ttl %s, err %+v, load count %d\n", key, value, ttl, err, reporter.CountLoad()) 40 | } 41 | 42 | func main() { 43 | // We provide some ways to report the status of cache. 44 | // Use NewCacheWithReport to create a cache with reporting features. 45 | cache, reporter := cachego.NewCacheWithReport( 46 | // Sometimes you may have several caches in one service. 47 | // You can set each name by WithCacheName and get the name from reporter. 48 | cachego.WithCacheName("test"), 49 | 50 | // For testing... 51 | cachego.WithMaxEntries(3), 52 | cachego.WithGC(100*time.Millisecond), 53 | 54 | // ReportMissed reports the missed key getting from cache. 55 | // ReportHit reports the hit entry getting from cache. 56 | // ReportGC reports the status of cache gc. 57 | // ReportLoad reports the result of loading. 58 | cachego.WithReportMissed(reportMissed), 59 | cachego.WithReportHit(reportHit), 60 | cachego.WithReportGC(reportGC), 61 | cachego.WithReportLoad(reportLoad), 62 | ) 63 | 64 | for i := 0; i < 5; i++ { 65 | key := strconv.Itoa(i) 66 | evictedValue := cache.Set(key, key, 10*time.Millisecond) 67 | fmt.Println(evictedValue) 68 | } 69 | 70 | for i := 0; i < 5; i++ { 71 | key := strconv.Itoa(i) 72 | value, ok := cache.Get(key) 73 | fmt.Println(value, ok) 74 | } 75 | 76 | time.Sleep(200 * time.Millisecond) 77 | 78 | value, err := cache.Load("key", time.Second, func() (value interface{}, err error) { 79 | return 666, io.EOF 80 | }) 81 | 82 | fmt.Println(value, err) 83 | 84 | // These are some useful methods of reporter. 85 | fmt.Println("CacheName:", reporter.CacheName()) 86 | fmt.Println("CacheType:", reporter.CacheType()) 87 | fmt.Println("CountMissed:", reporter.CountMissed()) 88 | fmt.Println("CountHit:", reporter.CountHit()) 89 | fmt.Println("CountGC:", reporter.CountGC()) 90 | fmt.Println("CountLoad:", reporter.CountLoad()) 91 | fmt.Println("CacheSize:", reporter.CacheSize()) 92 | fmt.Println("MissedRate:", reporter.MissedRate()) 93 | fmt.Println("HitRate:", reporter.HitRate()) 94 | } 95 | -------------------------------------------------------------------------------- /entry_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cachego 16 | 17 | import ( 18 | "fmt" 19 | "testing" 20 | "time" 21 | ) 22 | 23 | const ( 24 | testDurationGap = 10 * time.Microsecond 25 | ) 26 | 27 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestNewEntry$ 28 | func TestNewEntry(t *testing.T) { 29 | e := newEntry("key", "value", 0, now) 30 | 31 | if e.key != "key" { 32 | t.Fatalf("e.key %s is wrong", e.key) 33 | } 34 | 35 | if e.value.(string) != "value" { 36 | t.Fatalf("e.value %+v is wrong", e.value) 37 | } 38 | 39 | if e.expiration != 0 { 40 | t.Fatalf("e.expiration %+v != 0", e.expiration) 41 | } 42 | 43 | if fmt.Sprintf("%p", e.now) != fmt.Sprintf("%p", now) { 44 | t.Fatalf("e.now %p is wrong", e.now) 45 | } 46 | 47 | e = newEntry("k", "v", time.Second, now) 48 | expiration := time.Now().Add(time.Second).UnixNano() 49 | 50 | if e.key != "k" { 51 | t.Fatalf("e.key %s is wrong", e.key) 52 | } 53 | 54 | if e.value.(string) != "v" { 55 | t.Fatalf("e.value %+v is wrong", e.value) 56 | } 57 | 58 | if e.expiration == 0 { 59 | t.Fatal("e.expiration == 0") 60 | } 61 | 62 | if fmt.Sprintf("%p", e.now) != fmt.Sprintf("%p", now) { 63 | t.Fatalf("e.now %p is wrong", e.now) 64 | } 65 | 66 | // Keep one us for code running. 67 | if expiration < e.expiration || e.expiration < expiration-testDurationGap.Nanoseconds() { 68 | t.Fatalf("e.expiration %d != expiration %d", e.expiration, expiration) 69 | } 70 | } 71 | 72 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestEntrySetup$ 73 | func TestEntrySetup(t *testing.T) { 74 | e := newEntry("key", "value", 0, now) 75 | 76 | if e.key != "key" { 77 | t.Fatalf("e.key %s is wrong", e.key) 78 | } 79 | 80 | if e.value.(string) != "value" { 81 | t.Fatalf("e.value %+v is wrong", e.value) 82 | } 83 | 84 | if e.expiration != 0 { 85 | t.Fatalf("e.expiration %+v != 0", e.expiration) 86 | } 87 | 88 | ee := e 89 | e.setup("k", "v", time.Second) 90 | expiration := time.Now().Add(time.Second).UnixNano() 91 | 92 | if ee != e { 93 | t.Fatalf("ee %p != e %p", ee, e) 94 | } 95 | 96 | if e.key != "k" { 97 | t.Fatalf("e.key %s is wrong", e.key) 98 | } 99 | 100 | if e.value.(string) != "v" { 101 | t.Fatalf("e.value %+v is wrong", e.value) 102 | } 103 | 104 | if e.expiration == 0 { 105 | t.Fatal("e.expiration == 0") 106 | } 107 | 108 | // Keep one us for code running. 109 | if expiration < e.expiration || e.expiration < expiration-testDurationGap.Nanoseconds() { 110 | t.Fatalf("e.expiration %d != expiration %d", e.expiration, expiration) 111 | } 112 | } 113 | 114 | // go test -cover -run=^TestEntryExpired$ 115 | func TestEntryExpired(t *testing.T) { 116 | e := newEntry("", nil, time.Millisecond, now) 117 | 118 | if e.expired(0) { 119 | t.Fatal("e should be unexpired!") 120 | } 121 | 122 | time.Sleep(2 * time.Millisecond) 123 | 124 | if !e.expired(0) { 125 | t.Fatal("e should be expired!") 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /pkg/heap/heap.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package heap 16 | 17 | import ( 18 | "container/heap" 19 | ) 20 | 21 | const ( 22 | poppedIndex = -1 23 | ) 24 | 25 | // Item stores all information needed by heap including value. 26 | type Item struct { 27 | heap *Heap 28 | index int 29 | weight uint64 30 | 31 | // Value is the exact data storing in heap. 32 | Value interface{} 33 | } 34 | 35 | func newItem(heap *Heap, index int, weight uint64, value interface{}) *Item { 36 | return &Item{ 37 | heap: heap, 38 | index: index, 39 | weight: weight, 40 | Value: value, 41 | } 42 | } 43 | 44 | // Weight returns the weight of item. 45 | func (i *Item) Weight() uint64 { 46 | return i.weight 47 | } 48 | 49 | // Adjust adjusts weight of item in order to adjust heap. 50 | func (i *Item) Adjust(weight uint64) { 51 | i.weight = weight 52 | heap.Fix(i.heap.items, i.index) 53 | } 54 | 55 | type items []*Item 56 | 57 | func newItems(initialCap int) *items { 58 | is := make(items, 0, initialCap) 59 | heap.Init(&is) 60 | return &is 61 | } 62 | 63 | func (is *items) Len() int { 64 | return len(*is) 65 | } 66 | 67 | func (is *items) Less(i, j int) bool { 68 | return (*is)[i].weight < (*is)[j].weight 69 | } 70 | 71 | func (is *items) Swap(i, j int) { 72 | (*is)[i], (*is)[j] = (*is)[j], (*is)[i] 73 | (*is)[i].index = i 74 | (*is)[j].index = j 75 | } 76 | 77 | func (is *items) Push(x interface{}) { 78 | item := x.(*Item) 79 | item.index = len(*is) 80 | 81 | *is = append(*is, item) 82 | } 83 | 84 | func (is *items) Pop() interface{} { 85 | n := len(*is) 86 | item := (*is)[n-1] 87 | *is = (*is)[0 : n-1] 88 | 89 | item.index = poppedIndex // Already popped flag 90 | return item 91 | } 92 | 93 | // Heap uses items to build a heap which always pops the min weight item first. 94 | // It uses weight of item to sort items which may overflow because weight is an uint64 integer. 95 | // When overflow happens, its weight will turn to 0 and become one of the lightest items in heap. 96 | type Heap struct { 97 | items *items 98 | size int 99 | } 100 | 101 | // New creates a heap with initialCap of underlying slice. 102 | func New(initialCap int) *Heap { 103 | return &Heap{ 104 | items: newItems(initialCap), 105 | size: 0, 106 | } 107 | } 108 | 109 | // Push pushes a value with weight to item and returns the item. 110 | func (h *Heap) Push(weight uint64, value interface{}) *Item { 111 | index := len(*h.items) 112 | item := newItem(h, index, weight, value) 113 | 114 | heap.Push(h.items, item) 115 | h.size++ 116 | 117 | return item 118 | } 119 | 120 | // Pop pops the min item. 121 | func (h *Heap) Pop() *Item { 122 | if pop := heap.Pop(h.items); pop != nil { 123 | h.size-- 124 | return pop.(*Item) 125 | } 126 | 127 | return nil 128 | } 129 | 130 | // Remove removes item from heap and returns its value. 131 | func (h *Heap) Remove(item *Item) interface{} { 132 | if item.heap == h && item.index != poppedIndex { 133 | heap.Remove(h.items, item.index) 134 | h.size-- 135 | } 136 | 137 | return item.Value 138 | } 139 | 140 | // Size returns how many items storing in heap. 141 | func (h *Heap) Size() int { 142 | return h.size 143 | } 144 | -------------------------------------------------------------------------------- /load_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cachego 16 | 17 | import ( 18 | "strconv" 19 | "sync" 20 | "testing" 21 | "time" 22 | ) 23 | 24 | type testLoadCache struct { 25 | key string 26 | value interface{} 27 | ttl time.Duration 28 | 29 | loader *loader 30 | } 31 | 32 | func newTestLoadCache(singleflight bool) Cache { 33 | cache := &testLoadCache{ 34 | loader: newLoader(singleflight), 35 | } 36 | 37 | return cache 38 | } 39 | 40 | func (tlc *testLoadCache) Get(key string) (value interface{}, found bool) { 41 | return tlc.value, key == tlc.key 42 | } 43 | 44 | func (tlc *testLoadCache) Set(key string, value interface{}, ttl time.Duration) (evictedValue interface{}) { 45 | tlc.key = key 46 | tlc.value = value 47 | tlc.ttl = ttl 48 | 49 | return nil 50 | } 51 | 52 | func (tlc *testLoadCache) Remove(key string) (removedValue interface{}) { 53 | return nil 54 | } 55 | 56 | func (tlc *testLoadCache) Size() (size int) { 57 | return 1 58 | } 59 | 60 | func (tlc *testLoadCache) GC() (cleans int) { 61 | return 0 62 | } 63 | 64 | func (tlc *testLoadCache) Reset() {} 65 | 66 | func (tlc *testLoadCache) Load(key string, ttl time.Duration, load func() (value interface{}, err error)) (value interface{}, err error) { 67 | return tlc.loader.Load(key, ttl, load) 68 | } 69 | 70 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestNewLoader$ 71 | func TestNewLoader(t *testing.T) { 72 | loader := newLoader(false) 73 | if loader.group != nil { 74 | t.Fatalf("loader.group %+v != nil", loader.group) 75 | } 76 | 77 | loader = newLoader(true) 78 | if loader.group == nil { 79 | t.Fatal("loader.group == nil") 80 | } 81 | } 82 | 83 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestLoaderLoad$ 84 | func TestLoaderLoad(t *testing.T) { 85 | cache := newTestLoadCache(false) 86 | loadCount := 0 87 | 88 | for i := int64(0); i < 100; i++ { 89 | str := strconv.FormatInt(i, 10) 90 | 91 | value, err := cache.Load("key", time.Duration(i), func() (value interface{}, err error) { 92 | loadCount++ 93 | return str, nil 94 | }) 95 | 96 | if err != nil { 97 | t.Fatal(err) 98 | } 99 | 100 | if value.(string) != str { 101 | t.Fatalf("value.(string) %s != str %s", value.(string), str) 102 | } 103 | } 104 | 105 | if loadCount != 100 { 106 | t.Fatalf("loadCount %d != 100", loadCount) 107 | } 108 | 109 | cache = newTestLoadCache(true) 110 | loadCount = 0 111 | 112 | var errs []error 113 | var lock sync.Mutex 114 | var wg sync.WaitGroup 115 | 116 | for i := int64(0); i < 100; i++ { 117 | wg.Add(1) 118 | 119 | go func(i int64) { 120 | defer wg.Done() 121 | 122 | str := strconv.FormatInt(i, 10) 123 | 124 | _, err := cache.Load("key", time.Duration(i), func() (value interface{}, err error) { 125 | time.Sleep(time.Second) 126 | loadCount++ 127 | 128 | return str, nil 129 | }) 130 | 131 | if err != nil { 132 | lock.Lock() 133 | errs = append(errs, err) 134 | lock.Unlock() 135 | } 136 | }(i) 137 | } 138 | 139 | wg.Wait() 140 | 141 | for _, err := range errs { 142 | if err != nil { 143 | t.Fatal(err) 144 | } 145 | } 146 | 147 | if loadCount != 1 { 148 | t.Fatalf("loadCount %d != 1", loadCount) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /pkg/singleflight/singleflight_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package singleflight 16 | 17 | import ( 18 | "math/rand" 19 | "strconv" 20 | "sync" 21 | "sync/atomic" 22 | "testing" 23 | "time" 24 | ) 25 | 26 | func testGroupCall(t *testing.T, group *Group, concurrency int) { 27 | var wg sync.WaitGroup 28 | 29 | key := strconv.Itoa(rand.Int()) 30 | rightResult := int64(0) 31 | 32 | for i := 0; i < concurrency; i++ { 33 | wg.Add(1) 34 | 35 | go func(index int64) { 36 | defer wg.Done() 37 | 38 | result, err := group.Call(key, func() (interface{}, error) { 39 | time.Sleep(time.Second) 40 | atomic.StoreInt64(&rightResult, index) 41 | return index, nil 42 | }) 43 | 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | 48 | r := atomic.LoadInt64(&rightResult) 49 | if result != r { 50 | t.Fatalf("result %d != rightResult %d", result, r) 51 | } 52 | }(int64(i)) 53 | } 54 | 55 | wg.Wait() 56 | } 57 | 58 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestGroupCall$ 59 | func TestGroupCall(t *testing.T) { 60 | group := NewGroup(128) 61 | testGroupCall(t, group, 100000) 62 | } 63 | 64 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestGroupCallMultiKey$ 65 | func TestGroupCallMultiKey(t *testing.T) { 66 | group := NewGroup(128) 67 | 68 | var wg sync.WaitGroup 69 | for i := 0; i <= 100; i++ { 70 | wg.Add(1) 71 | 72 | go func() { 73 | defer wg.Done() 74 | testGroupCall(t, group, 1000) 75 | }() 76 | } 77 | 78 | wg.Wait() 79 | } 80 | 81 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestGroupDelete$ 82 | func TestGroupDelete(t *testing.T) { 83 | group := NewGroup(128) 84 | 85 | var wg sync.WaitGroup 86 | wg.Add(1) 87 | 88 | go group.Call("key", func() (interface{}, error) { 89 | wg.Done() 90 | 91 | time.Sleep(10 * time.Millisecond) 92 | return nil, nil 93 | }) 94 | 95 | wg.Wait() 96 | 97 | call := group.calls["key"] 98 | if call.deleted { 99 | t.Fatal("call.deleted is wrong") 100 | } 101 | 102 | group.Delete("key") 103 | 104 | if !call.deleted { 105 | t.Fatal("call.deleted is wrong") 106 | } 107 | 108 | if _, ok := group.calls["key"]; ok { 109 | t.Fatal("group.calls[\"key\"] is ok") 110 | } 111 | 112 | if len(group.calls) != 0 { 113 | t.Fatalf("len(group.calls) %d is wrong", len(group.calls)) 114 | } 115 | } 116 | 117 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestGroupReset$ 118 | func TestGroupReset(t *testing.T) { 119 | group := NewGroup(128) 120 | 121 | var wg sync.WaitGroup 122 | for i := 0; i < 10; i++ { 123 | wg.Add(1) 124 | key := strconv.Itoa(i) 125 | 126 | go group.Call(key, func() (interface{}, error) { 127 | wg.Done() 128 | 129 | time.Sleep(10 * time.Millisecond) 130 | return nil, nil 131 | }) 132 | } 133 | 134 | wg.Wait() 135 | 136 | calls := make([]*call, 0, len(group.calls)) 137 | for i := 0; i < 10; i++ { 138 | key := strconv.Itoa(i) 139 | 140 | call := group.calls[key] 141 | if call.deleted { 142 | t.Fatalf("key %s call.deleted is wrong", key) 143 | } 144 | 145 | calls = append(calls, call) 146 | } 147 | 148 | group.Reset() 149 | 150 | for i, call := range calls { 151 | if !call.deleted { 152 | t.Fatalf("i %d call.deleted is wrong", i) 153 | } 154 | 155 | key := strconv.Itoa(i) 156 | if _, ok := group.calls[key]; ok { 157 | t.Fatalf("group.calls[%s] is ok", key) 158 | } 159 | } 160 | 161 | if len(group.calls) != 0 { 162 | t.Fatalf("len(group.calls) %d is wrong", len(group.calls)) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /lru_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cachego 16 | 17 | import ( 18 | "math/rand" 19 | "strconv" 20 | "strings" 21 | "testing" 22 | "time" 23 | ) 24 | 25 | func newTestLRUCache() *lruCache { 26 | conf := newDefaultConfig() 27 | conf.maxEntries = maxTestEntries 28 | return newLRUCache(conf).(*lruCache) 29 | } 30 | 31 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestLRUCache$ 32 | func TestLRUCache(t *testing.T) { 33 | cache := newTestLRUCache() 34 | testCacheImplement(t, cache) 35 | } 36 | 37 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestLRUCacheEvict$ 38 | func TestLRUCacheEvict(t *testing.T) { 39 | cache := newTestLRUCache() 40 | 41 | for i := 0; i < cache.maxEntries*10; i++ { 42 | data := strconv.Itoa(i) 43 | evictedValue := cache.Set(data, data, time.Duration(i)*time.Second) 44 | 45 | if i >= cache.maxEntries && evictedValue == nil { 46 | t.Fatalf("i %d >= cache.maxEntries %d && evictedValue == nil", i, cache.maxEntries) 47 | } 48 | } 49 | 50 | if cache.Size() != cache.maxEntries { 51 | t.Fatalf("cache.Size() %d != cache.maxEntries %d", cache.Size(), cache.maxEntries) 52 | } 53 | 54 | for i := cache.maxEntries*10 - cache.maxEntries; i < cache.maxEntries*10; i++ { 55 | data := strconv.Itoa(i) 56 | value, ok := cache.Get(data) 57 | if !ok || value.(string) != data { 58 | t.Fatalf("!ok %+v || value.(string) %s != data %s", !ok, value.(string), data) 59 | } 60 | } 61 | 62 | i := cache.maxEntries*10 - cache.maxEntries 63 | element := cache.elementList.Back() 64 | for element != nil { 65 | entry := element.Value.(*entry) 66 | data := strconv.Itoa(i) 67 | 68 | if entry.key != data || entry.value.(string) != data { 69 | t.Fatalf("entry.key %s != data %s || entry.value.(string) %s != data %s", entry.key, data, entry.value.(string), data) 70 | } 71 | 72 | element = element.Prev() 73 | i++ 74 | } 75 | } 76 | 77 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestLRUCacheEvictSimulate$ 78 | func TestLRUCacheEvictSimulate(t *testing.T) { 79 | cache := newTestLRUCache() 80 | 81 | for i := 0; i < maxTestEntries; i++ { 82 | data := strconv.Itoa(i) 83 | cache.Set(data, data, NoTTL) 84 | } 85 | 86 | maxKeys := 10000 87 | keys := make([]string, 0, maxKeys) 88 | random := rand.New(rand.NewSource(time.Now().Unix())) 89 | 90 | for i := 0; i < maxKeys; i++ { 91 | key := strconv.Itoa(random.Intn(maxTestEntries)) 92 | keys = append(keys, key) 93 | } 94 | 95 | for _, key := range keys { 96 | cache.Get(key) 97 | } 98 | 99 | expectKeys := make([]string, maxTestEntries) 100 | index := len(expectKeys) - 1 101 | for i := len(keys) - 1; i > 0; i-- { 102 | exist := false 103 | 104 | for _, expectKey := range expectKeys { 105 | if keys[i] == expectKey { 106 | exist = true 107 | } 108 | } 109 | 110 | if !exist { 111 | expectKeys[index] = keys[i] 112 | index-- 113 | } 114 | } 115 | 116 | t.Log(expectKeys) 117 | 118 | var got strings.Builder 119 | element := cache.elementList.Back() 120 | for element != nil { 121 | got.WriteString(element.Value.(*entry).key) 122 | element = element.Prev() 123 | } 124 | 125 | expect := strings.Join(expectKeys, "") 126 | if strings.Compare(got.String(), expect) != 0 { 127 | t.Fatalf("got %s != expect %s", got.String(), expect) 128 | } 129 | 130 | for i := 0; i < maxTestEntries; i++ { 131 | data := strconv.Itoa(maxTestEntries*10 + i) 132 | evictedValue := cache.Set(data, data, NoTTL) 133 | 134 | if evictedValue.(string) != expectKeys[i] { 135 | t.Fatalf("evictedValue.(string) %s != expectKeys[i] %s", evictedValue.(string), expectKeys[i]) 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /standard.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cachego 16 | 17 | import ( 18 | "sync" 19 | "time" 20 | ) 21 | 22 | type standardCache struct { 23 | *config 24 | 25 | entries map[string]*entry 26 | lock sync.RWMutex 27 | 28 | loader *loader 29 | } 30 | 31 | func newStandardCache(conf *config) Cache { 32 | cache := &standardCache{ 33 | config: conf, 34 | entries: make(map[string]*entry, mapInitialCap), 35 | loader: newLoader(conf.singleflight), 36 | } 37 | 38 | return cache 39 | } 40 | 41 | func (sc *standardCache) get(key string) (value interface{}, found bool) { 42 | entry, ok := sc.entries[key] 43 | if ok && !entry.expired(0) { 44 | return entry.value, true 45 | } 46 | 47 | return nil, false 48 | } 49 | 50 | func (sc *standardCache) evict() (evictedValue interface{}) { 51 | for key := range sc.entries { 52 | return sc.remove(key) 53 | } 54 | 55 | return nil 56 | } 57 | 58 | func (sc *standardCache) set(key string, value interface{}, ttl time.Duration) (evictedValue interface{}) { 59 | entry, ok := sc.entries[key] 60 | if ok { 61 | entry.setup(key, value, ttl) 62 | return nil 63 | } 64 | 65 | if sc.maxEntries > 0 && sc.size() >= sc.maxEntries { 66 | evictedValue = sc.evict() 67 | } 68 | 69 | sc.entries[key] = newEntry(key, value, ttl, sc.now) 70 | return evictedValue 71 | } 72 | 73 | func (sc *standardCache) remove(key string) (removedValue interface{}) { 74 | entry, ok := sc.entries[key] 75 | if !ok { 76 | return nil 77 | } 78 | 79 | delete(sc.entries, key) 80 | return entry.value 81 | } 82 | 83 | func (sc *standardCache) size() (size int) { 84 | return len(sc.entries) 85 | } 86 | 87 | func (sc *standardCache) gc() (cleans int) { 88 | now := sc.now() 89 | scans := 0 90 | 91 | for _, entry := range sc.entries { 92 | scans++ 93 | 94 | if entry.expired(now) { 95 | delete(sc.entries, entry.key) 96 | cleans++ 97 | } 98 | 99 | if sc.maxScans > 0 && scans >= sc.maxScans { 100 | break 101 | } 102 | } 103 | 104 | return cleans 105 | } 106 | 107 | func (sc *standardCache) reset() { 108 | sc.entries = make(map[string]*entry, mapInitialCap) 109 | sc.loader.Reset() 110 | } 111 | 112 | // Get gets the value of key from cache and returns value if found. 113 | // See Cache interface. 114 | func (sc *standardCache) Get(key string) (value interface{}, found bool) { 115 | sc.lock.RLock() 116 | defer sc.lock.RUnlock() 117 | 118 | return sc.get(key) 119 | } 120 | 121 | // Set sets key and value to cache with ttl and returns evicted value if exists and unexpired. 122 | // See Cache interface. 123 | func (sc *standardCache) Set(key string, value interface{}, ttl time.Duration) (evictedValue interface{}) { 124 | sc.lock.Lock() 125 | defer sc.lock.Unlock() 126 | 127 | return sc.set(key, value, ttl) 128 | } 129 | 130 | // Remove removes key and returns the removed value of key. 131 | // See Cache interface. 132 | func (sc *standardCache) Remove(key string) (removedValue interface{}) { 133 | sc.lock.Lock() 134 | defer sc.lock.Unlock() 135 | 136 | return sc.remove(key) 137 | } 138 | 139 | // Size returns the count of keys in cache. 140 | // See Cache interface. 141 | func (sc *standardCache) Size() (size int) { 142 | sc.lock.RLock() 143 | defer sc.lock.RUnlock() 144 | 145 | return sc.size() 146 | } 147 | 148 | // GC cleans the expired keys in cache and returns the exact count cleaned. 149 | // See Cache interface. 150 | func (sc *standardCache) GC() (cleans int) { 151 | sc.lock.Lock() 152 | defer sc.lock.Unlock() 153 | 154 | return sc.gc() 155 | } 156 | 157 | // Reset resets cache to initial status which is like a new cache. 158 | // See Cache interface. 159 | func (sc *standardCache) Reset() { 160 | sc.lock.Lock() 161 | defer sc.lock.Unlock() 162 | 163 | sc.reset() 164 | } 165 | 166 | // Load loads a value by load function and sets it to cache. 167 | // Returns an error if load failed. 168 | func (sc *standardCache) Load(key string, ttl time.Duration, load func() (value interface{}, err error)) (value interface{}, err error) { 169 | value, err = sc.loader.Load(key, ttl, load) 170 | if err != nil { 171 | return value, err 172 | } 173 | 174 | sc.Set(key, value, ttl) 175 | return value, nil 176 | } 177 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ## ✒ 历史版本的特性介绍 (Features in old versions) 2 | 3 | ### v1.0.0 4 | 5 | > 此版本发布于 2025-03-15 6 | 7 | * 稳定的 API 版本 8 | * 调整快速时钟的代码,更改包名为 fastclock 9 | 10 | ### v0.6.1 11 | 12 | > 此版本发布于 2024-01-18 13 | 14 | * 调整代码 15 | 16 | ### v0.6.0-alpha 17 | 18 | > 此版本发布于 2024-01-13 19 | 20 | * 受小徒弟的灵感激发,进行 loader 代码的调整 21 | * 把 cache 结构去掉,精简这部分设计 22 | 23 | ### v0.5.0 24 | 25 | > 此版本发布于 2023-11-30 26 | 27 | * 调整单元测试代码 28 | * API 进入稳定观察期 29 | 30 | ### v0.4.12 31 | 32 | > 此版本发布于 2023-06-12 33 | 34 | * 给 Reporter 增加缓存 gc 运行间隔方法,主要用于监控不同缓存的 gc 运行间隔配置情况 35 | 36 | ### v0.4.11 37 | 38 | > 此版本发布于 2023-05-10 39 | 40 | * 给 CacheType 增加 String 方法 41 | 42 | ### v0.4.10 43 | 44 | > 此版本发布于 2023-05-10 45 | 46 | * 给 Reporter 增加缓存类型方法,主要用于监控不同类型缓存的使用情况 47 | 48 | ### v0.4.9 49 | 50 | > 此版本发布于 2023-05-09 51 | 52 | * 给 Reporter 增加缓存分片数量方法,主要用于监控缓存分片数量 53 | 54 | ### v0.4.8 55 | 56 | > 此版本发布于 2023-03-13 57 | 58 | * 增加缓存名字配置,主要用于区分每个监控数据的来源 59 | 60 | ### v0.4.7 61 | 62 | > 此版本发布于 2023-03-08 63 | 64 | * 默认开启 10 分钟的 GC(因为从线上使用情况来看开启的概率远远大于不开启) 65 | * 默认限制最多缓存 10w 个键值对,如果需要不限制,可以使用 WithMaxEntries 指定为 0 66 | * 完善文档和使用案例,特别是强调了 GC 和单飞的使用 67 | * 祝我的妈妈,我的老婆女神节、妇女节都快乐! 68 | 69 | ### v0.4.6 70 | 71 | > 此版本发布于 2023-03-01 72 | 73 | * 全新设计版本 74 | 75 | ### v0.4.5-alpha 76 | 77 | > 此版本发布于 2023-02-21 78 | 79 | * 完善 report 机制 80 | 81 | ### v0.4.4-alpha 82 | 83 | > 此版本发布于 2023-02-20 84 | 85 | * 重新设计 report 机制 86 | 87 | ### v0.4.3-alpha 88 | 89 | > 此版本发布于 2023-02-14 90 | 91 | * 修改 option 部分限制 92 | 93 | ### v0.4.2-alpha 94 | 95 | > 此版本发布于 2023-02-06 96 | 97 | * 提取 now 和 hash 到缓存级别配置 98 | * 提供定时缓存时间的机制,可选快速时钟 99 | 100 | ### v0.4.1-alpha 101 | 102 | > 此版本发布于 2023-02-01 103 | 104 | * 基本完成全部单元测试 105 | 106 | ### v0.4.0-alpha 107 | 108 | > 此版本发布于 2023-02-01 109 | 110 | * 设计 Cache 接口,Get 方法用 bool 判断,单个锁结构 111 | * 提供 ShardingCache 实现,实现 Cache 接口,细化锁粒度 112 | * 提供多种接口实现,包括 standard,lru,lfu 等 113 | * 提供 load 方法,集成 singleflight 进行数据加载 114 | * 操作提供 option 机制,ttl 使用 option 设置,默认值使用 option 设置,最大遍历次数使用 option 设置 115 | * Delete 方法改 Remove 并返回被删除的 value 116 | * DeleteAll 方法改 Reset 117 | * GC 方法保留,去除 AutoGC 方法 118 | * 检查 pkg 代码,完善单元测试,提高覆盖率 119 | * 清理废话注释,完善 examples 和性能测试 120 | * 增加 report 机制用于监控缓存的情况 121 | 122 | ### v0.3.7 123 | 124 | > 此版本发布于 2022-10-04 125 | 126 | * 提取 Index 函数,用户可以自定义哈希算法 127 | * 大量优化代码风格 128 | 129 | ### v0.3.6 130 | 131 | > 此版本发布于 2022-05-09 132 | 133 | * 主机使用 manjaro 系统开发 134 | * 加入不重载 onMissed 的选项,可以不缓存数据,只使用单飞 135 | 136 | ### v0.3.5-alpha 137 | 138 | > 此版本发布于 2022-05-06 139 | 140 | * 主机使用 deepin 系统开发 141 | * 加入不重载 onMissed 的选项,可以不缓存数据,只使用单飞 142 | 143 | ### v0.3.4 144 | 145 | > 此版本发布于 2022-03-13 146 | 147 | * 去除 AutoSet 方法,使用 Task 机制代替 148 | * 希望开源可以帮助更多软件国产化!!! 149 | 150 | ### v0.3.3-alpha 151 | 152 | > 此版本发布于 2022-02-27 153 | 154 | * 大量精简无用冗余设计代码 155 | * 去除 AutoSet 方法,使用 Task 机制代替 156 | * 去除文件头描述和修改协议描述 157 | 158 | ### v0.3.2 159 | 160 | > 此版本发布于 2022-01-31 161 | 162 | * 祝大家除夕夜快乐,新年行大运 :) 163 | 164 | ### v0.3.1-alpha 165 | 166 | > 此版本发布于 2022-01-06 167 | 168 | * 完善使用案例和文档 169 | 170 | ### v0.3.0-alpha 171 | 172 | > 此版本发布于 2021-12-25 173 | 174 | * 祝大家圣诞节快乐!!! 175 | * 去除 GetWithTTL 方法 176 | * 重新设计 AutoSet 方法,引入 option 机制 177 | * 加入 singleflight 机制 178 | * 优化 value 使用,复用内存、代码可读性 179 | * Set 引入 option 机制 180 | * Get 引入 option 机制 181 | * 优化 Set 性能,实行 value 复用 182 | 183 | ### v0.2.5 184 | 185 | > 此版本发布于 2021-10-10 186 | 187 | * 修改 sync.Mutex 的使用方式 188 | 189 | ### v0.2.4 190 | 191 | > 此版本发布于 2021-10-07 192 | 193 | * 设置 segmentSize 的选项增加了参数检验,防止用户设置不合理导致 segment 分布不均匀 194 | 195 | ### v0.2.3-alpha 196 | 197 | > 此版本发布于 2021-07-01 198 | 199 | * 祝党成立一百周年快乐! 200 | * 新增 AutoSet 方法定时加载数据到缓存,以实现超高热点数据不穿透到数据库的极速访问 201 | * AutoGc 方法退出时会调用 Ticker.Stop 终止计时器,防止泄露 202 | 203 | ### v0.2.2 204 | 205 | > 此版本发布于 2021-05-01 206 | 207 | * 祝大家五一劳动节快乐! 208 | * 新增 GetWithLoad 方便缓存失效自动加载 209 | * AutoGc 返回参数修改为 chan<- struct{} 210 | 211 | ### v0.2.1 212 | 213 | > 此版本发布于 2021-04-08 214 | 215 | * 增加 debug 网络调试点,目前提供了多个调试点,开发时更方便缓存验证 216 | 217 | ### v0.2.0-alpha 218 | 219 | > 此版本发布于 2021-04-05 220 | 221 | * 加入 option function 创建模式,创建缓存实例的同时可以进行定制化的配置 222 | * ~~性能优化 - 引入 value 实例池~~ 223 | 经过测试,这个实例池没有带来性能提升,反而影响了写入的性能,说明目前的性能瓶颈不在实例创建上,所以取消该特性 224 | * 增加 debug 网络调试点,目前暂时没有提供具体的调试点,将在后续版本中推出 225 | 226 | ### v0.1.3 227 | 228 | > 此版本发布于 2021-03-14 229 | 230 | * 优化 hash 算法,现在的算法使得数据分布更加均衡 231 | 232 | ### v0.1.2 233 | 234 | > 此版本发布于 2021-03-12 235 | 236 | * 更改 index 为位运算 237 | 238 | ### v0.1.1 239 | 240 | > 此版本发布于 2020-11-07 241 | 242 | * 更改 value 中的 alive 方法逻辑,适配低版本 SDK 243 | 244 | ### v0.1.0 245 | 246 | > 此版本发布于 2020-09-13 247 | 248 | * 简化设计,现在的我偏向于反设计 249 | * 加入分段锁,使用更细粒度的锁机制保证更高的缓存性能 250 | * 初步完善哨兵清理机制,配合分段锁,优化数据清理效率 251 | 252 | ### v0.0.1 253 | 254 | > 此版本发布于 2020-03-17 255 | 256 | * 简单实现一个并发访问安全、支持自动清理过期数据的缓存器 257 | * 支持懒清理机制,每一次访问的时候判断是否过期 258 | * 支持哨兵清理机制,每隔一定的时间间隔进行清理过期数据 259 | * 基础特性和高级特性分离设计模式,减少新用户学习上手难度 260 | * 链式编程友好的 API 设计,在一定程度上提供了很高的代码可读性 -------------------------------------------------------------------------------- /pkg/heap/heap_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package heap 16 | 17 | import ( 18 | "math" 19 | "math/rand" 20 | "sort" 21 | "testing" 22 | "time" 23 | ) 24 | 25 | func newTestData(count int) []int { 26 | random := rand.New(rand.NewSource(time.Now().Unix())) 27 | 28 | var data []int 29 | for i := 0; i < count; i++ { 30 | data = append(data, random.Intn(count*10)) 31 | } 32 | 33 | return data 34 | } 35 | 36 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestItem$ 37 | func TestItem(t *testing.T) { 38 | heap := New(64) 39 | 40 | item1 := heap.Push(1, 11) 41 | if item1.index != 0 { 42 | t.Fatalf("item1.index %d is wrong", item1.index) 43 | } 44 | 45 | item2 := heap.Push(2, 22) 46 | if item2.index != 1 { 47 | t.Fatalf("item2.index %d is wrong", item2.index) 48 | } 49 | 50 | item3 := heap.Push(3, 33) 51 | if item3.index != 2 { 52 | t.Fatalf("item3.index %d is wrong", item3.index) 53 | } 54 | 55 | if item1.Weight() != 1 || item1.Value.(int) != 11 { 56 | t.Fatalf("item1.Weight() %d is wrong || item1.Value.(int) %d is wrong", item1.Weight(), item1.Value.(int)) 57 | } 58 | 59 | if item2.Weight() != 2 || item2.Value.(int) != 22 { 60 | t.Fatalf("item2.Weight() %d is wrong || item2.Value.(int) %d is wrong", item2.Weight(), item2.Value.(int)) 61 | } 62 | 63 | if item3.Weight() != 3 || item3.Value.(int) != 33 { 64 | t.Fatalf("item3.Weight() %d is wrong || item3.Value.(int) %d is wrong", item3.Weight(), item3.Value.(int)) 65 | } 66 | 67 | item1.Adjust(111) 68 | if item1.Weight() != 111 || item1.index != 1 { 69 | t.Fatalf("item1.Weight() %d is wrong || item1.index %d is wrong", item1.Weight(), item1.index) 70 | } 71 | 72 | if item2.index != 0 { 73 | t.Fatalf("item2.index %d is wrong", item2.index) 74 | } 75 | 76 | if item3.index != 2 { 77 | t.Fatalf("item3.index %d is wrong", item3.index) 78 | } 79 | 80 | item2.Adjust(222) 81 | 82 | weight := uint64(math.MaxUint64) 83 | item3.Adjust(weight + 1) 84 | 85 | if item3.Weight() != 0 { 86 | t.Fatalf("item3.Weight() %d is wrong", item3.Weight()) 87 | } 88 | 89 | expect := []int{33, 11, 22} 90 | index := 0 91 | 92 | for heap.Size() > 0 { 93 | num := heap.Pop().Value.(int) 94 | if num != expect[index] { 95 | t.Fatalf("num %d != expect[%d] %d", num, index, expect[index]) 96 | } 97 | 98 | index++ 99 | } 100 | 101 | item1.weight = math.MaxUint64 102 | for i := uint64(1); i <= 3; i++ { 103 | item1.weight = item1.weight + 1 104 | if item1.Weight() != i-1 { 105 | t.Fatalf("item1.Weight() %d != (i %d - 1)", item1.Weight(), i) 106 | } 107 | } 108 | } 109 | 110 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestHeap$ 111 | func TestHeap(t *testing.T) { 112 | data := newTestData(10) 113 | t.Log(data) 114 | 115 | heap := New(64) 116 | for _, num := range data { 117 | heap.Push(uint64(num), num) 118 | } 119 | 120 | if heap.Size() != len(data) { 121 | t.Fatalf("heap.Size() %d != len(data) %d", heap.Size(), len(data)) 122 | } 123 | 124 | sort.Ints(data) 125 | t.Log(data) 126 | 127 | index := 0 128 | for heap.Size() > 0 { 129 | num := heap.Pop().Value.(int) 130 | if num != data[index] { 131 | t.Fatalf("num %d != data[%d] %d", num, index, data[index]) 132 | } 133 | 134 | index++ 135 | } 136 | 137 | if heap.Size() != 0 { 138 | t.Fatalf("heap.Size() %d is wrong", heap.Size()) 139 | } 140 | 141 | rand.Shuffle(len(data), func(i, j int) { 142 | data[i], data[j] = data[j], data[i] 143 | }) 144 | 145 | items := make([]*Item, 0, len(data)) 146 | for _, num := range data { 147 | item := heap.Push(uint64(num), num) 148 | items = append(items, item) 149 | } 150 | 151 | if heap.Size() != len(data) { 152 | t.Fatalf("heap.Size() %d != len(data) %d", heap.Size(), len(data)) 153 | } 154 | 155 | if len(items) != len(data) { 156 | t.Fatalf("len(items) %d != len(data) %d", len(items), len(data)) 157 | } 158 | 159 | for i, num := range data { 160 | value := heap.Remove(items[i]) 161 | if value.(int) != num { 162 | t.Fatalf("value.(int) %d != num %d", value.(int), num) 163 | } 164 | } 165 | 166 | if heap.Size() != 0 { 167 | t.Fatalf("heap.Size() %d is wrong", heap.Size()) 168 | } 169 | 170 | item := &Item{heap: heap, index: poppedIndex, Value: 123} 171 | if value := heap.Remove(item); value.(int) != 123 { 172 | t.Fatalf("value.(int) %d is wrong", value.(int)) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /lfu_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cachego 16 | 17 | import ( 18 | "math/rand" 19 | "sort" 20 | "strconv" 21 | "testing" 22 | "time" 23 | ) 24 | 25 | func newTestLFUCache() *lfuCache { 26 | conf := newDefaultConfig() 27 | conf.maxEntries = maxTestEntries 28 | 29 | return newLFUCache(conf).(*lfuCache) 30 | } 31 | 32 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestLFUCache$ 33 | func TestLFUCache(t *testing.T) { 34 | cache := newTestLFUCache() 35 | testCacheImplement(t, cache) 36 | } 37 | 38 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestLFUCacheEvict$ 39 | func TestLFUCacheEvict(t *testing.T) { 40 | cache := newTestLFUCache() 41 | 42 | for i := 0; i < cache.maxEntries*10; i++ { 43 | data := strconv.Itoa(i) 44 | evictedValue := cache.Set(data, data, time.Duration(i)*time.Second) 45 | 46 | if i >= cache.maxEntries && evictedValue == nil { 47 | t.Fatalf("i %d >= cache.maxEntries %d && evictedValue == nil", i, cache.maxEntries) 48 | } 49 | } 50 | 51 | if cache.Size() != cache.maxEntries { 52 | t.Fatalf("cache.Size() %d != cache.maxEntries %d", cache.Size(), cache.maxEntries) 53 | } 54 | 55 | for i := cache.maxEntries*10 - cache.maxEntries; i < cache.maxEntries*10; i++ { 56 | for j := 0; j < i; j++ { 57 | data := strconv.Itoa(i) 58 | 59 | cache.Set(data, data, time.Duration(i)*time.Second) 60 | cache.Get(data) 61 | } 62 | } 63 | 64 | for i := cache.maxEntries*10 - cache.maxEntries; i < cache.maxEntries*10; i++ { 65 | data := strconv.Itoa(i) 66 | 67 | value, ok := cache.Get(data) 68 | if !ok || value.(string) != data { 69 | t.Fatalf("!ok %+v || value.(string) %s != data %s", !ok, value.(string), data) 70 | } 71 | } 72 | 73 | i := cache.maxEntries*10 - cache.maxEntries 74 | 75 | for cache.itemHeap.Size() > 0 { 76 | item := cache.itemHeap.Pop() 77 | entry := item.Value.(*entry) 78 | data := strconv.Itoa(i) 79 | 80 | if entry.key != data || entry.value.(string) != data { 81 | t.Fatalf("entry.key %s != data %s || entry.value.(string) %s != data %s", entry.key, data, entry.value.(string), data) 82 | } 83 | 84 | i++ 85 | } 86 | } 87 | 88 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestLFUCacheEvictSimulate$ 89 | func TestLFUCacheEvictSimulate(t *testing.T) { 90 | cache := newTestLFUCache() 91 | 92 | for i := 0; i < maxTestEntries; i++ { 93 | data := strconv.Itoa(i) 94 | cache.Set(data, data, NoTTL) 95 | } 96 | 97 | maxKeys := 10000 98 | keys := make([]string, 0, maxKeys) 99 | random := rand.New(rand.NewSource(time.Now().Unix())) 100 | 101 | for i := 0; i < maxKeys; i++ { 102 | key := strconv.Itoa(random.Intn(maxTestEntries)) 103 | keys = append(keys, key) 104 | } 105 | 106 | type times struct { 107 | key string 108 | count int 109 | } 110 | 111 | counts := make([]times, maxTestEntries) 112 | for _, key := range keys { 113 | cache.Get(key) 114 | 115 | i, err := strconv.ParseInt(key, 10, 64) 116 | if err != nil { 117 | t.Fatal(err) 118 | } 119 | 120 | counts[i].key = key 121 | counts[i].count++ 122 | } 123 | 124 | sort.Slice(counts, func(i, j int) bool { 125 | return counts[i].count < counts[j].count 126 | }) 127 | 128 | t.Log(counts) 129 | 130 | expect := make([]string, 0, maxTestEntries) 131 | for i := 0; i < maxTestEntries; i++ { 132 | data := strconv.Itoa(maxTestEntries*10 + i) 133 | expect = append(expect, data) 134 | evictedValue := cache.Set(data, data, NoTTL) 135 | 136 | for j := 0; j < maxKeys+i; j++ { 137 | cache.Get(data) 138 | } 139 | 140 | if evictedValue.(string) != counts[i].key { 141 | found := false 142 | 143 | // Counts may repeat and the sequence may not the same as we think. 144 | for _, count := range counts { 145 | if count.key != evictedValue.(string) { 146 | continue 147 | } 148 | 149 | // Count doesn't equal means something wrong happens. 150 | if count.count != counts[i].count { 151 | t.Fatalf("evictedValue.(string) %s != counts[i].key %s", evictedValue.(string), counts[i].key) 152 | } 153 | 154 | found = true 155 | break 156 | } 157 | 158 | if !found { 159 | t.Fatalf("evictedValue %s not found in counts %+v", evictedValue.(string), counts) 160 | } 161 | } 162 | } 163 | 164 | index := 0 165 | for cache.itemHeap.Size() > 0 { 166 | item := cache.itemHeap.Pop() 167 | 168 | if item.Value.(*entry).key != expect[index] { 169 | t.Fatalf("item.Value.(*entry).key %s != expect[index] %s", item.Value.(*entry).key, expect[index]) 170 | } 171 | 172 | if item.Weight() != uint64(maxKeys+index) { 173 | t.Fatalf("item.Weight() %d != uint64(maxKeys + index) %d", item.Weight(), uint64(maxKeys+index)) 174 | } 175 | 176 | index++ 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cachego 16 | 17 | import ( 18 | "context" 19 | "time" 20 | 21 | "github.com/FishGoddess/cachego/pkg/task" 22 | ) 23 | 24 | const ( 25 | // NoTTL means a key is never expired. 26 | NoTTL = 0 27 | ) 28 | 29 | var ( 30 | newCaches = map[CacheType]func(conf *config) Cache{ 31 | standard: newStandardCache, 32 | lru: newLRUCache, 33 | lfu: newLFUCache, 34 | } 35 | ) 36 | 37 | // Cache is the core interface of cachego. 38 | // We provide some implements including standard cache and sharding cache. 39 | type Cache interface { 40 | // Get gets the value of key from cache and returns value if found. 41 | // A nil value will be returned if key doesn't exist in cache. 42 | // Notice that we won't remove expired keys in get method, so you should remove them manually or set a limit of keys. 43 | // The reason why we won't remove expired keys in get method is for higher re-usability, because we often set a new value 44 | // of expired key after getting it (so we can reuse the memory of entry). 45 | Get(key string) (value interface{}, found bool) 46 | 47 | // Set sets key and value to cache with ttl and returns evicted value if exists. 48 | // See NoTTL if you want your key is never expired. 49 | Set(key string, value interface{}, ttl time.Duration) (evictedValue interface{}) 50 | 51 | // Remove removes key and returns the removed value of key. 52 | // A nil value will be returned if key doesn't exist in cache. 53 | Remove(key string) (removedValue interface{}) 54 | 55 | // Size returns the count of keys in cache. 56 | // The result may be different in different implements. 57 | Size() (size int) 58 | 59 | // GC cleans the expired keys in cache and returns the exact count cleaned. 60 | // The exact cleans depend on implements, however, all implements should have a limit of scanning. 61 | GC() (cleans int) 62 | 63 | // Reset resets cache to initial status which is like a new cache. 64 | Reset() 65 | 66 | // Load loads a key with ttl to cache and returns an error if failed. 67 | // We recommend you use this method to load missed keys to cache, 68 | // because it may use singleflight to reduce the times calling load function. 69 | Load(key string, ttl time.Duration, load func() (value interface{}, err error)) (value interface{}, err error) 70 | } 71 | 72 | func newCache(withReport bool, opts ...Option) (cache Cache, reporter *Reporter) { 73 | conf := newDefaultConfig() 74 | applyOptions(conf, opts) 75 | 76 | newCache, ok := newCaches[conf.cacheType] 77 | if !ok { 78 | panic("cachego: cache type doesn't exist") 79 | } 80 | 81 | if conf.shardings > 0 { 82 | cache = newShardingCache(conf, newCache) 83 | } else { 84 | cache = newCache(conf) 85 | } 86 | 87 | if withReport { 88 | cache, reporter = report(conf, cache) 89 | } 90 | 91 | if conf.gcDuration > 0 { 92 | RunGCTask(cache, conf.gcDuration) 93 | } 94 | 95 | return cache, reporter 96 | } 97 | 98 | // NewCache creates a cache with options. 99 | // By default, it will create a standard cache which uses one lock to solve data race. 100 | // It may cause a big performance problem in high concurrency. 101 | // You can use WithShardings to create a sharding cache which is good for concurrency. 102 | // Also, you can use options to specify the type of cache to others, such as lru. 103 | // Use NewCacheWithReporter to get a reporter for use if you want. 104 | func NewCache(opts ...Option) (cache Cache) { 105 | cache, _ = newCache(false, opts...) 106 | return cache 107 | } 108 | 109 | // NewCacheWithReport creates a cache and a reporter with options. 110 | // By default, it will create a standard cache which uses one lock to solve data race. 111 | // It may cause a big performance problem in high concurrency. 112 | // You can use WithShardings to create a sharding cache which is good for concurrency. 113 | // Also, you can use options to specify the type of cache to others, such as lru. 114 | func NewCacheWithReport(opts ...Option) (cache Cache, reporter *Reporter) { 115 | return newCache(true, opts...) 116 | } 117 | 118 | // RunGCTask runs a gc task in a new goroutine and returns a cancel function to cancel the task. 119 | // However, you don't need to call it manually for most time, instead, use options is a better choice. 120 | // Making it a public function is for more customizations in some situations. 121 | // For example, using options to run gc task is un-cancelable, so you can use it to run gc task by your own 122 | // and get a cancel function to cancel the gc task. 123 | func RunGCTask(cache Cache, duration time.Duration) (cancel func()) { 124 | fn := func(ctx context.Context) { 125 | cache.GC() 126 | } 127 | 128 | ctx := context.Background() 129 | ctx, cancel = context.WithCancel(ctx) 130 | 131 | go task.New(fn).Context(ctx).Duration(duration).Run() 132 | return cancel 133 | } 134 | -------------------------------------------------------------------------------- /option.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cachego 16 | 17 | import ( 18 | "time" 19 | ) 20 | 21 | // Option applies to config and sets some values to config. 22 | type Option func(conf *config) 23 | 24 | func (o Option) applyTo(conf *config) { 25 | o(conf) 26 | } 27 | 28 | func applyOptions(conf *config, opts []Option) { 29 | for _, opt := range opts { 30 | opt.applyTo(conf) 31 | } 32 | } 33 | 34 | // WithCacheName returns an option setting the cacheName of config. 35 | func WithCacheName(cacheName string) Option { 36 | return func(conf *config) { 37 | conf.cacheName = cacheName 38 | } 39 | } 40 | 41 | // WithLRU returns an option setting the type of cache to lru. 42 | // Notice that lru cache must have max entries limit, so you have to specify a maxEntries. 43 | func WithLRU(maxEntries int) Option { 44 | return func(conf *config) { 45 | conf.cacheType = lru 46 | conf.maxEntries = maxEntries 47 | } 48 | } 49 | 50 | // WithLFU returns an option setting the type of cache to lfu. 51 | // Notice that lfu cache must have max entries limit, so you have to specify a maxEntries. 52 | func WithLFU(maxEntries int) Option { 53 | return func(conf *config) { 54 | conf.cacheType = lfu 55 | conf.maxEntries = maxEntries 56 | } 57 | } 58 | 59 | // WithShardings returns an option setting the sharding count of cache. 60 | // Negative value means no sharding. 61 | func WithShardings(shardings int) Option { 62 | return func(conf *config) { 63 | conf.shardings = shardings 64 | } 65 | } 66 | 67 | // WithDisableSingleflight returns an option turning off singleflight mode of cache. 68 | func WithDisableSingleflight() Option { 69 | return func(conf *config) { 70 | conf.singleflight = false 71 | } 72 | } 73 | 74 | // WithGC returns an option setting the duration of cache gc. 75 | // Negative value means no gc. 76 | func WithGC(gcDuration time.Duration) Option { 77 | return func(conf *config) { 78 | conf.gcDuration = gcDuration 79 | } 80 | } 81 | 82 | // WithMaxScans returns an option setting the max scans of cache. 83 | // Negative value means no limit. 84 | func WithMaxScans(maxScans int) Option { 85 | return func(conf *config) { 86 | conf.maxScans = maxScans 87 | } 88 | } 89 | 90 | // WithMaxEntries returns an option setting the max entries of cache. 91 | // Negative value means no limit. 92 | func WithMaxEntries(maxEntries int) Option { 93 | return func(conf *config) { 94 | conf.maxEntries = maxEntries 95 | } 96 | } 97 | 98 | // WithNow returns an option setting the now function of cache. 99 | // A now function should return a nanosecond unix time. 100 | func WithNow(now func() int64) Option { 101 | return func(conf *config) { 102 | if now != nil { 103 | conf.now = now 104 | } 105 | } 106 | } 107 | 108 | // WithHash returns an option setting the hash function of cache. 109 | // A hash function should return the hash code of key. 110 | func WithHash(hash func(key string) int) Option { 111 | return func(conf *config) { 112 | if hash != nil { 113 | conf.hash = hash 114 | } 115 | } 116 | } 117 | 118 | // WithRecordMissed returns an option setting the recordMissed of config. 119 | func WithRecordMissed(recordMissed bool) Option { 120 | return func(conf *config) { 121 | conf.recordMissed = recordMissed 122 | } 123 | } 124 | 125 | // WithRecordHit returns an option setting the recordHit of config. 126 | func WithRecordHit(recordHit bool) Option { 127 | return func(conf *config) { 128 | conf.recordHit = recordHit 129 | } 130 | } 131 | 132 | // WithRecordGC returns an option setting the recordGC of config. 133 | func WithRecordGC(recordGC bool) Option { 134 | return func(conf *config) { 135 | conf.recordGC = recordGC 136 | } 137 | } 138 | 139 | // WithRecordLoad returns an option setting the recordLoad of config. 140 | func WithRecordLoad(recordLoad bool) Option { 141 | return func(conf *config) { 142 | conf.recordLoad = recordLoad 143 | } 144 | } 145 | 146 | // WithReportMissed returns an option setting the reportMissed of config. 147 | func WithReportMissed(reportMissed func(reporter *Reporter, key string)) Option { 148 | return func(conf *config) { 149 | conf.reportMissed = reportMissed 150 | } 151 | } 152 | 153 | // WithReportHit returns an option setting the reportHit of config. 154 | func WithReportHit(reportHit func(reporter *Reporter, key string, value interface{})) Option { 155 | return func(conf *config) { 156 | conf.reportHit = reportHit 157 | } 158 | } 159 | 160 | // WithReportGC returns an option setting the reportGC of config. 161 | func WithReportGC(reportGC func(reporter *Reporter, cost time.Duration, cleans int)) Option { 162 | return func(conf *config) { 163 | conf.reportGC = reportGC 164 | } 165 | } 166 | 167 | // WithReportLoad returns an option setting the reportLoad of config. 168 | func WithReportLoad(reportLoad func(reporter *Reporter, key string, value interface{}, ttl time.Duration, err error)) Option { 169 | return func(conf *config) { 170 | conf.reportLoad = reportLoad 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /lfu.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cachego 16 | 17 | import ( 18 | "sync" 19 | "time" 20 | 21 | "github.com/FishGoddess/cachego/pkg/heap" 22 | ) 23 | 24 | type lfuCache struct { 25 | *config 26 | 27 | itemMap map[string]*heap.Item 28 | itemHeap *heap.Heap 29 | lock sync.RWMutex 30 | 31 | loader *loader 32 | } 33 | 34 | func newLFUCache(conf *config) Cache { 35 | if conf.maxEntries <= 0 { 36 | panic("cachego: lfu cache must specify max entries") 37 | } 38 | 39 | cache := &lfuCache{ 40 | config: conf, 41 | itemMap: make(map[string]*heap.Item, mapInitialCap), 42 | itemHeap: heap.New(sliceInitialCap), 43 | loader: newLoader(conf.singleflight), 44 | } 45 | 46 | return cache 47 | } 48 | 49 | func (lc *lfuCache) unwrap(item *heap.Item) *entry { 50 | entry, ok := item.Value.(*entry) 51 | if !ok { 52 | panic("cachego: failed to unwrap lfu item's value to entry") 53 | } 54 | 55 | return entry 56 | } 57 | 58 | func (lc *lfuCache) evict() (evictedValue interface{}) { 59 | if item := lc.itemHeap.Pop(); item != nil { 60 | return lc.removeItem(item) 61 | } 62 | 63 | return nil 64 | } 65 | 66 | func (lc *lfuCache) get(key string) (value interface{}, found bool) { 67 | item, ok := lc.itemMap[key] 68 | if !ok { 69 | return nil, false 70 | } 71 | 72 | entry := lc.unwrap(item) 73 | if entry.expired(0) { 74 | return nil, false 75 | } 76 | 77 | item.Adjust(item.Weight() + 1) 78 | return entry.value, true 79 | } 80 | 81 | func (lc *lfuCache) set(key string, value interface{}, ttl time.Duration) (evictedValue interface{}) { 82 | item, ok := lc.itemMap[key] 83 | if ok { 84 | entry := lc.unwrap(item) 85 | entry.setup(key, value, ttl) 86 | 87 | item.Adjust(item.Weight() + 1) 88 | return nil 89 | } 90 | 91 | if lc.maxEntries > 0 && lc.itemHeap.Size() >= lc.maxEntries { 92 | evictedValue = lc.evict() 93 | } 94 | 95 | item = lc.itemHeap.Push(0, newEntry(key, value, ttl, lc.now)) 96 | lc.itemMap[key] = item 97 | 98 | return evictedValue 99 | } 100 | 101 | func (lc *lfuCache) removeItem(item *heap.Item) (removedValue interface{}) { 102 | entry := lc.unwrap(item) 103 | 104 | delete(lc.itemMap, entry.key) 105 | lc.itemHeap.Remove(item) 106 | 107 | return entry.value 108 | } 109 | 110 | func (lc *lfuCache) remove(key string) (removedValue interface{}) { 111 | if item, ok := lc.itemMap[key]; ok { 112 | return lc.removeItem(item) 113 | } 114 | 115 | return nil 116 | } 117 | 118 | func (lc *lfuCache) size() (size int) { 119 | return len(lc.itemMap) 120 | } 121 | 122 | func (lc *lfuCache) gc() (cleans int) { 123 | now := lc.now() 124 | scans := 0 125 | 126 | for _, item := range lc.itemMap { 127 | scans++ 128 | 129 | if entry := lc.unwrap(item); entry.expired(now) { 130 | lc.removeItem(item) 131 | cleans++ 132 | } 133 | 134 | if lc.maxScans > 0 && scans >= lc.maxScans { 135 | break 136 | } 137 | } 138 | 139 | return cleans 140 | } 141 | 142 | func (lc *lfuCache) reset() { 143 | lc.itemMap = make(map[string]*heap.Item, mapInitialCap) 144 | lc.itemHeap = heap.New(sliceInitialCap) 145 | 146 | lc.loader.Reset() 147 | } 148 | 149 | // Get gets the value of key from cache and returns value if found. 150 | // See Cache interface. 151 | func (lc *lfuCache) Get(key string) (value interface{}, found bool) { 152 | lc.lock.Lock() 153 | defer lc.lock.Unlock() 154 | 155 | return lc.get(key) 156 | } 157 | 158 | // Set sets key and value to cache with ttl and returns evicted value if exists and unexpired. 159 | // See Cache interface. 160 | func (lc *lfuCache) Set(key string, value interface{}, ttl time.Duration) (evictedValue interface{}) { 161 | lc.lock.Lock() 162 | defer lc.lock.Unlock() 163 | 164 | return lc.set(key, value, ttl) 165 | } 166 | 167 | // Remove removes key and returns the removed value of key. 168 | // See Cache interface. 169 | func (lc *lfuCache) Remove(key string) (removedValue interface{}) { 170 | lc.lock.Lock() 171 | defer lc.lock.Unlock() 172 | 173 | return lc.remove(key) 174 | } 175 | 176 | // Size returns the count of keys in cache. 177 | // See Cache interface. 178 | func (lc *lfuCache) Size() (size int) { 179 | lc.lock.RLock() 180 | defer lc.lock.RUnlock() 181 | 182 | return lc.size() 183 | } 184 | 185 | // GC cleans the expired keys in cache and returns the exact count cleaned. 186 | // See Cache interface. 187 | func (lc *lfuCache) GC() (cleans int) { 188 | lc.lock.Lock() 189 | defer lc.lock.Unlock() 190 | 191 | return lc.gc() 192 | } 193 | 194 | // Reset resets cache to initial status which is like a new cache. 195 | // See Cache interface. 196 | func (lc *lfuCache) Reset() { 197 | lc.lock.Lock() 198 | defer lc.lock.Unlock() 199 | 200 | lc.reset() 201 | } 202 | 203 | // Load loads a value by load function and sets it to cache. 204 | // Returns an error if load failed. 205 | func (lc *lfuCache) Load(key string, ttl time.Duration, load func() (value interface{}, err error)) (value interface{}, err error) { 206 | value, err = lc.loader.Load(key, ttl, load) 207 | if err != nil { 208 | return value, err 209 | } 210 | 211 | lc.Set(key, value, ttl) 212 | return value, nil 213 | } 214 | -------------------------------------------------------------------------------- /lru.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cachego 16 | 17 | import ( 18 | "container/list" 19 | "sync" 20 | "time" 21 | ) 22 | 23 | type lruCache struct { 24 | *config 25 | 26 | elementMap map[string]*list.Element 27 | elementList *list.List 28 | lock sync.RWMutex 29 | 30 | loader *loader 31 | } 32 | 33 | func newLRUCache(conf *config) Cache { 34 | if conf.maxEntries <= 0 { 35 | panic("cachego: lru cache must specify max entries") 36 | } 37 | 38 | cache := &lruCache{ 39 | config: conf, 40 | elementMap: make(map[string]*list.Element, mapInitialCap), 41 | elementList: list.New(), 42 | loader: newLoader(conf.singleflight), 43 | } 44 | 45 | return cache 46 | } 47 | 48 | func (lc *lruCache) unwrap(element *list.Element) *entry { 49 | entry, ok := element.Value.(*entry) 50 | if !ok { 51 | panic("cachego: failed to unwrap lru element's value to entry") 52 | } 53 | 54 | return entry 55 | } 56 | 57 | func (lc *lruCache) evict() (evictedValue interface{}) { 58 | if element := lc.elementList.Back(); element != nil { 59 | return lc.removeElement(element) 60 | } 61 | 62 | return nil 63 | } 64 | 65 | func (lc *lruCache) get(key string) (value interface{}, found bool) { 66 | element, ok := lc.elementMap[key] 67 | if !ok { 68 | return nil, false 69 | } 70 | 71 | entry := lc.unwrap(element) 72 | if entry.expired(0) { 73 | return nil, false 74 | } 75 | 76 | lc.elementList.MoveToFront(element) 77 | return entry.value, true 78 | } 79 | 80 | func (lc *lruCache) set(key string, value interface{}, ttl time.Duration) (evictedValue interface{}) { 81 | element, ok := lc.elementMap[key] 82 | if ok { 83 | entry := lc.unwrap(element) 84 | entry.setup(key, value, ttl) 85 | 86 | lc.elementList.MoveToFront(element) 87 | return nil 88 | } 89 | 90 | if lc.maxEntries > 0 && lc.elementList.Len() >= lc.maxEntries { 91 | evictedValue = lc.evict() 92 | } 93 | 94 | element = lc.elementList.PushFront(newEntry(key, value, ttl, lc.now)) 95 | lc.elementMap[key] = element 96 | 97 | return evictedValue 98 | } 99 | 100 | func (lc *lruCache) removeElement(element *list.Element) (removedValue interface{}) { 101 | entry := lc.unwrap(element) 102 | 103 | delete(lc.elementMap, entry.key) 104 | lc.elementList.Remove(element) 105 | 106 | return entry.value 107 | } 108 | 109 | func (lc *lruCache) remove(key string) (removedValue interface{}) { 110 | if element, ok := lc.elementMap[key]; ok { 111 | return lc.removeElement(element) 112 | } 113 | 114 | return nil 115 | } 116 | 117 | func (lc *lruCache) size() (size int) { 118 | return len(lc.elementMap) 119 | } 120 | 121 | func (lc *lruCache) gc() (cleans int) { 122 | now := lc.now() 123 | scans := 0 124 | 125 | for _, element := range lc.elementMap { 126 | scans++ 127 | 128 | if entry := lc.unwrap(element); entry.expired(now) { 129 | lc.removeElement(element) 130 | cleans++ 131 | } 132 | 133 | if lc.maxScans > 0 && scans >= lc.maxScans { 134 | break 135 | } 136 | } 137 | 138 | return cleans 139 | } 140 | 141 | func (lc *lruCache) reset() { 142 | lc.elementMap = make(map[string]*list.Element, mapInitialCap) 143 | lc.elementList = list.New() 144 | 145 | lc.loader.Reset() 146 | } 147 | 148 | // Get gets the value of key from cache and returns value if found. 149 | // See Cache interface. 150 | func (lc *lruCache) Get(key string) (value interface{}, found bool) { 151 | lc.lock.Lock() 152 | defer lc.lock.Unlock() 153 | 154 | return lc.get(key) 155 | } 156 | 157 | // Set sets key and value to cache with ttl and returns evicted value if exists and unexpired. 158 | // See Cache interface. 159 | func (lc *lruCache) Set(key string, value interface{}, ttl time.Duration) (evictedValue interface{}) { 160 | lc.lock.Lock() 161 | defer lc.lock.Unlock() 162 | 163 | return lc.set(key, value, ttl) 164 | } 165 | 166 | // Remove removes key and returns the removed value of key. 167 | // See Cache interface. 168 | func (lc *lruCache) Remove(key string) (removedValue interface{}) { 169 | lc.lock.Lock() 170 | defer lc.lock.Unlock() 171 | 172 | return lc.remove(key) 173 | } 174 | 175 | // Size returns the count of keys in cache. 176 | // See Cache interface. 177 | func (lc *lruCache) Size() (size int) { 178 | lc.lock.RLock() 179 | defer lc.lock.RUnlock() 180 | 181 | return lc.size() 182 | } 183 | 184 | // GC cleans the expired keys in cache and returns the exact count cleaned. 185 | // See Cache interface. 186 | func (lc *lruCache) GC() (cleans int) { 187 | lc.lock.Lock() 188 | defer lc.lock.Unlock() 189 | 190 | return lc.gc() 191 | } 192 | 193 | // Reset resets cache to initial status which is like a new cache. 194 | // See Cache interface. 195 | func (lc *lruCache) Reset() { 196 | lc.lock.Lock() 197 | defer lc.lock.Unlock() 198 | 199 | lc.reset() 200 | } 201 | 202 | // Load loads a value by load function and sets it to cache. 203 | // Returns an error if load failed. 204 | func (lc *lruCache) Load(key string, ttl time.Duration, load func() (value interface{}, err error)) (value interface{}, err error) { 205 | value, err = lc.loader.Load(key, ttl, load) 206 | if err != nil { 207 | return value, err 208 | } 209 | 210 | lc.Set(key, value, ttl) 211 | return value, nil 212 | } 213 | -------------------------------------------------------------------------------- /report.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cachego 16 | 17 | import ( 18 | "sync/atomic" 19 | "time" 20 | ) 21 | 22 | // Reporter stores some values for reporting. 23 | type Reporter struct { 24 | conf *config 25 | cache Cache 26 | 27 | missedCount uint64 28 | hitCount uint64 29 | gcCount uint64 30 | loadCount uint64 31 | } 32 | 33 | func (r *Reporter) increaseMissedCount() { 34 | atomic.AddUint64(&r.missedCount, 1) 35 | } 36 | 37 | func (r *Reporter) increaseHitCount() { 38 | atomic.AddUint64(&r.hitCount, 1) 39 | } 40 | 41 | func (r *Reporter) increaseGCCount() { 42 | atomic.AddUint64(&r.gcCount, 1) 43 | } 44 | 45 | func (r *Reporter) increaseLoadCount() { 46 | atomic.AddUint64(&r.loadCount, 1) 47 | } 48 | 49 | // CacheName returns the name of cache. 50 | // You can use WithCacheName to set cache's name. 51 | func (r *Reporter) CacheName() string { 52 | return r.conf.cacheName 53 | } 54 | 55 | // CacheType returns the type of cache. 56 | // See CacheType. 57 | func (r *Reporter) CacheType() CacheType { 58 | return r.conf.cacheType 59 | } 60 | 61 | // CacheShardings returns the shardings of cache. 62 | // You can use WithShardings to set cache's shardings. 63 | // Zero shardings means cache is non-sharding. 64 | func (r *Reporter) CacheShardings() int { 65 | return r.conf.shardings 66 | } 67 | 68 | // CacheGC returns the gc duration of cache. 69 | // You can use WithGC to set cache's gc duration. 70 | // Zero duration means cache disables gc. 71 | func (r *Reporter) CacheGC() time.Duration { 72 | return r.conf.gcDuration 73 | } 74 | 75 | // CacheSize returns the size of cache. 76 | func (r *Reporter) CacheSize() int { 77 | return r.cache.Size() 78 | } 79 | 80 | // CountMissed returns the missed count. 81 | func (r *Reporter) CountMissed() uint64 { 82 | return atomic.LoadUint64(&r.missedCount) 83 | } 84 | 85 | // CountHit returns the hit count. 86 | func (r *Reporter) CountHit() uint64 { 87 | return atomic.LoadUint64(&r.hitCount) 88 | } 89 | 90 | // CountGC returns the gc count. 91 | func (r *Reporter) CountGC() uint64 { 92 | return atomic.LoadUint64(&r.gcCount) 93 | } 94 | 95 | // CountLoad returns the load count. 96 | func (r *Reporter) CountLoad() uint64 { 97 | return atomic.LoadUint64(&r.loadCount) 98 | } 99 | 100 | // MissedRate returns the missed rate. 101 | func (r *Reporter) MissedRate() float64 { 102 | hit := r.CountHit() 103 | missed := r.CountMissed() 104 | 105 | total := hit + missed 106 | if total <= 0 { 107 | return 0.0 108 | } 109 | 110 | return float64(missed) / float64(total) 111 | } 112 | 113 | // HitRate returns the hit rate. 114 | func (r *Reporter) HitRate() float64 { 115 | hit := r.CountHit() 116 | missed := r.CountMissed() 117 | 118 | total := hit + missed 119 | if total <= 0 { 120 | return 0.0 121 | } 122 | 123 | return float64(hit) / float64(total) 124 | } 125 | 126 | type reportableCache struct { 127 | *config 128 | *Reporter 129 | } 130 | 131 | func report(conf *config, cache Cache) (Cache, *Reporter) { 132 | reporter := &Reporter{ 133 | conf: conf, 134 | cache: cache, 135 | hitCount: 0, 136 | missedCount: 0, 137 | gcCount: 0, 138 | loadCount: 0, 139 | } 140 | 141 | cache = &reportableCache{ 142 | config: conf, 143 | Reporter: reporter, 144 | } 145 | 146 | return cache, reporter 147 | } 148 | 149 | // Get gets the value of key from cache and returns value if found. 150 | func (rc *reportableCache) Get(key string) (value interface{}, found bool) { 151 | value, found = rc.cache.Get(key) 152 | 153 | if found { 154 | if rc.recordHit { 155 | rc.increaseHitCount() 156 | } 157 | 158 | if rc.reportHit != nil { 159 | rc.reportHit(rc.Reporter, key, value) 160 | } 161 | } else { 162 | if rc.recordMissed { 163 | rc.increaseMissedCount() 164 | } 165 | 166 | if rc.reportMissed != nil { 167 | rc.reportMissed(rc.Reporter, key) 168 | } 169 | } 170 | 171 | return value, found 172 | } 173 | 174 | // Set sets key and value to cache with ttl and returns evicted value if exists and unexpired. 175 | // See Cache interface. 176 | func (rc *reportableCache) Set(key string, value interface{}, ttl time.Duration) (evictedValue interface{}) { 177 | return rc.cache.Set(key, value, ttl) 178 | } 179 | 180 | // Remove removes key and returns the removed value of key. 181 | // See Cache interface. 182 | func (rc *reportableCache) Remove(key string) (removedValue interface{}) { 183 | return rc.cache.Remove(key) 184 | } 185 | 186 | // Size returns the count of keys in cache. 187 | // See Cache interface. 188 | func (rc *reportableCache) Size() (size int) { 189 | return rc.cache.Size() 190 | } 191 | 192 | // GC cleans the expired keys in cache and returns the exact count cleaned. 193 | // See Cache interface. 194 | func (rc *reportableCache) GC() (cleans int) { 195 | if rc.recordGC { 196 | rc.increaseGCCount() 197 | } 198 | 199 | if rc.reportGC == nil { 200 | return rc.cache.GC() 201 | } 202 | 203 | begin := rc.now() 204 | cleans = rc.cache.GC() 205 | end := rc.now() 206 | 207 | cost := time.Duration(end - begin) 208 | rc.reportGC(rc.Reporter, cost, cleans) 209 | 210 | return cleans 211 | } 212 | 213 | // Reset resets cache to initial status which is like a new cache. 214 | // See Cache interface. 215 | func (rc *reportableCache) Reset() { 216 | rc.cache.Reset() 217 | } 218 | 219 | // Load loads a key with ttl to cache and returns an error if failed. 220 | // See Cache interface. 221 | func (rc *reportableCache) Load(key string, ttl time.Duration, load func() (value interface{}, err error)) (value interface{}, err error) { 222 | value, err = rc.cache.Load(key, ttl, load) 223 | 224 | if rc.recordLoad { 225 | rc.increaseLoadCount() 226 | } 227 | 228 | if rc.reportLoad != nil { 229 | rc.reportLoad(rc.Reporter, key, value, ttl, err) 230 | } 231 | 232 | return value, err 233 | } 234 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🍰 cachego 2 | 3 | [![Go Doc](_icons/godoc.svg)](https://pkg.go.dev/github.com/FishGoddess/cachego) 4 | [![License](_icons/license.svg)](https://www.apache.org/licenses/LICENSE-2.0.html) 5 | [![Coverage](_icons/coverage.svg)](_icons/coverage.svg) 6 | ![Test](https://github.com/FishGoddess/cachego/actions/workflows/test.yml/badge.svg) 7 | 8 | **cachego** 是一个拥有分片机制的轻量级内存缓存库,API 友好,支持多种数据淘汰机制,可以应用于所有的 [GoLang](https://golang.org) 应用程序中。 9 | 10 | > 目前已经在多个线上服务中运行稳定,服务日常请求过万 qps,最高抵御过 96w/s qps 的冲击,欢迎使用!👏🏻 11 | 12 | [Read me in English](./README.en.md). 13 | 14 | ### 🕹 功能特性 15 | 16 | * 以键值对形式缓存数据,极简的 API 设计风格 17 | * 引入 option function 模式,简化创建缓存参数 18 | * 提供 ttl 过期机制,支持限制键值对数量 19 | * 提供 lru 清理机制,提供 lfu 清理机制 20 | * 提供锁粒度更细的分片缓存,具有非常高的并发性能 21 | * 支持懒清理机制,每一次访问的时候判断是否过期 22 | * 支持哨兵清理机制,每隔一定的时间间隔进行清理 23 | * 自带 singleflight 机制,减少缓存穿透的伤害 24 | * 自带定时任务封装,方便热数据定时加载到缓存 25 | * 支持上报缓存状况,可自定义多个缓存上报点 26 | * 自带快速时钟,支持纳秒级获取时间 27 | 28 | _历史版本的特性请查看 [HISTORY.md](./HISTORY.md)。未来版本的新特性和计划请查看 [FUTURE.md](./FUTURE.md)。_ 29 | 30 | ### 🚀 安装方式 31 | 32 | ```bash 33 | $ go get -u github.com/FishGoddess/cachego 34 | ``` 35 | 36 | ### 💡 参考案例 37 | 38 | ```go 39 | package main 40 | 41 | import ( 42 | "fmt" 43 | "time" 44 | 45 | "github.com/FishGoddess/cachego" 46 | ) 47 | 48 | func main() { 49 | // Use NewCache function to create a cache. 50 | // By default, it creates a standard cache which evicts entries randomly. 51 | // Use WithShardings to shard cache to several parts for higher performance. 52 | // Use WithGC to clean expired entries every 10 minutes. 53 | cache := cachego.NewCache(cachego.WithGC(10*time.Minute), cachego.WithShardings(64)) 54 | 55 | // Set an entry to cache with ttl. 56 | cache.Set("key", 123, time.Second) 57 | 58 | // Get an entry from cache. 59 | value, ok := cache.Get("key") 60 | fmt.Println(value, ok) // 123 true 61 | 62 | // Check how many entries stores in cache. 63 | size := cache.Size() 64 | fmt.Println(size) // 1 65 | 66 | // Clean expired entries. 67 | cleans := cache.GC() 68 | fmt.Println(cleans) // 1 69 | 70 | // Set an entry which doesn't have ttl. 71 | cache.Set("key", 123, cachego.NoTTL) 72 | 73 | // Remove an entry. 74 | removedValue := cache.Remove("key") 75 | fmt.Println(removedValue) // 123 76 | 77 | // Reset resets cache to initial status. 78 | cache.Reset() 79 | 80 | // Get value from cache and load it to cache if not found. 81 | value, ok = cache.Get("key") 82 | if !ok { 83 | // Loaded entry will be set to cache and returned. 84 | // By default, it will use singleflight. 85 | value, _ = cache.Load("key", time.Second, func() (value interface{}, err error) { 86 | return 666, nil 87 | }) 88 | } 89 | 90 | fmt.Println(value) // 666 91 | 92 | // You can use WithLRU to specify the type of cache to lru. 93 | // Also, try WithLFU if you want to use lfu to evict data. 94 | cache = cachego.NewCache(cachego.WithLRU(100)) 95 | cache = cachego.NewCache(cachego.WithLFU(100)) 96 | 97 | // Use NewCacheWithReport to create a cache with report. 98 | cache, reporter := cachego.NewCacheWithReport(cachego.WithCacheName("test")) 99 | fmt.Println(reporter.CacheName()) 100 | fmt.Println(reporter.CacheType()) 101 | } 102 | ``` 103 | 104 | _更多使用案例请查看 [_examples](./_examples) 目录。_ 105 | 106 | ### 🔥 性能测试 107 | 108 | ```bash 109 | $ make bench 110 | ``` 111 | 112 | ```bash 113 | goos: darwin 114 | goarch: amd64 115 | cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz 116 | 117 | BenchmarkCachegoGet-12 25214618 47.2 ns/op 0 B/op 0 allocs/op 118 | BenchmarkCachegoGetLRU-12 8169417 149.0 ns/op 0 B/op 0 allocs/op 119 | BenchmarkCachegoGetLFU-12 7071300 171.6 ns/op 0 B/op 0 allocs/op 120 | BenchmarkCachegoGetSharding-12 72568048 16.8 ns/op 0 B/op 0 allocs/op 121 | BenchmarkGcacheGet-12 4765129 252.1 ns/op 16 B/op 1 allocs/op 122 | BenchmarkGcacheGetLRU-12 5735739 214.0 ns/op 16 B/op 1 allocs/op 123 | BenchmarkGcacheGetLFU-12 4830048 250.8 ns/op 16 B/op 1 allocs/op 124 | BenchmarkEcacheGet-12 11515140 101.0 ns/op 0 B/op 0 allocs/op 125 | BenchmarkEcache2Get-12 12255506 95.6 ns/op 0 B/op 0 allocs/op 126 | BenchmarkBigcacheGet-12 21711988 60.4 ns/op 7 B/op 2 allocs/op 127 | BenchmarkFreecacheGet-12 24903388 44.3 ns/op 27 B/op 2 allocs/op 128 | BenchmarkGoCacheGet-12 19818014 61.4 ns/op 0 B/op 0 allocs/op 129 | 130 | BenchmarkCachegoSet-12 5743768 209.6 ns/op 16 B/op 1 allocs/op 131 | BenchmarkCachegoSetLRU-12 6105316 189.9 ns/op 16 B/op 1 allocs/op 132 | BenchmarkCachegoSetLFU-12 5505601 217.2 ns/op 16 B/op 1 allocs/op 133 | BenchmarkCachegoSetSharding-12 39012607 31.2 ns/op 16 B/op 1 allocs/op 134 | BenchmarkGcacheSet-12 3326841 365.3 ns/op 56 B/op 3 allocs/op 135 | BenchmarkGcacheSetLRU-12 3471307 318.7 ns/op 56 B/op 3 allocs/op 136 | BenchmarkGcacheSetLFU-12 3896512 335.1 ns/op 56 B/op 3 allocs/op 137 | BenchmarkEcacheSet-12 7318136 167.5 ns/op 32 B/op 2 allocs/op 138 | BenchmarkEcache2Set-12 7020867 175.7 ns/op 32 B/op 2 allocs/op 139 | BenchmarkBigcacheSet-12 4107825 268.9 ns/op 55 B/op 0 allocs/op 140 | BenchmarkFreecacheSet-12 44181687 28.4 ns/op 0 B/op 0 allocs/op 141 | BenchmarkGoCacheSet-12 4921483 249.0 ns/op 16 B/op 1 allocs/op 142 | ``` 143 | 144 | > 注:Ecache 只有 LRU 模式,v1 和 v2 两个版本;Freecache 默认是 256 分片,无法调节为 1 个分片进行对比测试。 145 | 146 | > 测试文件:[_examples/performance_test.go](./_examples/performance_test.go)。 147 | 148 | ### 👥 贡献者 149 | 150 | * [cristiane](https://gitee.com/cristiane):提供 hash 算法的优化建议 151 | * [hzy15610046011](https://gitee.com/hzy15610046011):提供架构设计文档和图片 152 | * [chen661](https://gitee.com/chen661):提供 segmentSize 设置选项的参数限制想法 153 | 154 | 如果您觉得 cachego 缺少您需要的功能,请不要犹豫,马上参与进来,发起一个 _**issue**_。 155 | 156 | [![Star History Chart](https://api.star-history.com/svg?repos=fishgoddess/cachego&type=Date)](https://star-history.com/#fishgoddess/cachego&Date) 157 | -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 | # 🍰 cachego 2 | 3 | [![Go Doc](_icons/godoc.svg)](https://pkg.go.dev/github.com/FishGoddess/cachego) 4 | [![License](_icons/license.svg)](https://www.apache.org/licenses/LICENSE-2.0.html) 5 | [![Coverage](_icons/coverage.svg)](_icons/coverage.svg) 6 | ![Test](https://github.com/FishGoddess/cachego/actions/workflows/test.yml/badge.svg) 7 | 8 | **cachego** is an api friendly memory-based cache for [GoLang](https://golang.org) applications. 9 | 10 | > It has been used by many services in production, all services are running stable, and the highest qps in services is 11 | > 96w/s, so just use it if you want! 👏🏻 12 | 13 | [阅读中文版的 Read me](./README.md). 14 | 15 | ### 🕹 Features 16 | 17 | * Cache as entries with minimalist API design 18 | * Use option function mode to customize the creation of cache 19 | * TTL supports and max size of entries in cache 20 | * LRU supports and LFU supports. 21 | * Use sharding lock mechanism to provide a high performance in concurrency 22 | * Lazy cleanup supports, expired before accessing 23 | * Sentinel cleanup supports, cleaning up at fixed duration 24 | * Singleflight supports, which can decrease the times of cache penetration 25 | * Timer task supports, which is convenient to load data to cache 26 | * Report supports, providing several reporting points 27 | * Fast clock supports, fetching current time in nanoseconds 28 | 29 | _Check [HISTORY.md](./HISTORY.md) and [FUTURE.md](./FUTURE.md) to get more information._ 30 | 31 | ### 🚀 Installation 32 | 33 | ```bash 34 | $ go get -u github.com/FishGoddess/cachego 35 | ``` 36 | 37 | ### 💡 Examples 38 | 39 | ```go 40 | package main 41 | 42 | import ( 43 | "fmt" 44 | "time" 45 | 46 | "github.com/FishGoddess/cachego" 47 | ) 48 | 49 | func main() { 50 | // Use NewCache function to create a cache. 51 | // By default, it creates a standard cache which evicts entries randomly. 52 | // Use WithShardings to shard cache to several parts for higher performance. 53 | // Use WithGC to clean expired entries every 10 minutes. 54 | cache := cachego.NewCache(cachego.WithGC(10*time.Minute), cachego.WithShardings(64)) 55 | 56 | // Set an entry to cache with ttl. 57 | cache.Set("key", 123, time.Second) 58 | 59 | // Get an entry from cache. 60 | value, ok := cache.Get("key") 61 | fmt.Println(value, ok) // 123 true 62 | 63 | // Check how many entries stores in cache. 64 | size := cache.Size() 65 | fmt.Println(size) // 1 66 | 67 | // Clean expired entries. 68 | cleans := cache.GC() 69 | fmt.Println(cleans) // 1 70 | 71 | // Set an entry which doesn't have ttl. 72 | cache.Set("key", 123, cachego.NoTTL) 73 | 74 | // Remove an entry. 75 | removedValue := cache.Remove("key") 76 | fmt.Println(removedValue) // 123 77 | 78 | // Reset resets cache to initial status. 79 | cache.Reset() 80 | 81 | // Get value from cache and load it to cache if not found. 82 | value, ok = cache.Get("key") 83 | if !ok { 84 | // Loaded entry will be set to cache and returned. 85 | // By default, it will use singleflight. 86 | value, _ = cache.Load("key", time.Second, func() (value interface{}, err error) { 87 | return 666, nil 88 | }) 89 | } 90 | 91 | fmt.Println(value) // 666 92 | 93 | // You can use WithLRU to specify the type of cache to lru. 94 | // Also, try WithLFU if you want to use lfu to evict data. 95 | cache = cachego.NewCache(cachego.WithLRU(100)) 96 | cache = cachego.NewCache(cachego.WithLFU(100)) 97 | 98 | // Use NewCacheWithReport to create a cache with report. 99 | cache, reporter := cachego.NewCacheWithReport(cachego.WithCacheName("test")) 100 | fmt.Println(reporter.CacheName()) 101 | fmt.Println(reporter.CacheType()) 102 | } 103 | ``` 104 | 105 | _Check more examples in [_examples](./_examples)._ 106 | 107 | ### 🔥 Benchmarks 108 | 109 | ```bash 110 | $ make bench 111 | ``` 112 | 113 | ```bash 114 | goos: darwin 115 | goarch: amd64 116 | cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz 117 | 118 | BenchmarkCachegoGet-12 25214618 47.2 ns/op 0 B/op 0 allocs/op 119 | BenchmarkCachegoGetLRU-12 8169417 149.0 ns/op 0 B/op 0 allocs/op 120 | BenchmarkCachegoGetLFU-12 7071300 171.6 ns/op 0 B/op 0 allocs/op 121 | BenchmarkCachegoGetSharding-12 72568048 16.8 ns/op 0 B/op 0 allocs/op 122 | BenchmarkGcacheGet-12 4765129 252.1 ns/op 16 B/op 1 allocs/op 123 | BenchmarkGcacheGetLRU-12 5735739 214.0 ns/op 16 B/op 1 allocs/op 124 | BenchmarkGcacheGetLFU-12 4830048 250.8 ns/op 16 B/op 1 allocs/op 125 | BenchmarkEcacheGet-12 11515140 101.0 ns/op 0 B/op 0 allocs/op 126 | BenchmarkEcache2Get-12 12255506 95.6 ns/op 0 B/op 0 allocs/op 127 | BenchmarkBigcacheGet-12 21711988 60.4 ns/op 7 B/op 2 allocs/op 128 | BenchmarkFreecacheGet-12 24903388 44.3 ns/op 27 B/op 2 allocs/op 129 | BenchmarkGoCacheGet-12 19818014 61.4 ns/op 0 B/op 0 allocs/op 130 | 131 | BenchmarkCachegoSet-12 5743768 209.6 ns/op 16 B/op 1 allocs/op 132 | BenchmarkCachegoSetLRU-12 6105316 189.9 ns/op 16 B/op 1 allocs/op 133 | BenchmarkCachegoSetLFU-12 5505601 217.2 ns/op 16 B/op 1 allocs/op 134 | BenchmarkCachegoSetSharding-12 39012607 31.2 ns/op 16 B/op 1 allocs/op 135 | BenchmarkGcacheSet-12 3326841 365.3 ns/op 56 B/op 3 allocs/op 136 | BenchmarkGcacheSetLRU-12 3471307 318.7 ns/op 56 B/op 3 allocs/op 137 | BenchmarkGcacheSetLFU-12 3896512 335.1 ns/op 56 B/op 3 allocs/op 138 | BenchmarkEcacheSet-12 7318136 167.5 ns/op 32 B/op 2 allocs/op 139 | BenchmarkEcache2Set-12 7020867 175.7 ns/op 32 B/op 2 allocs/op 140 | BenchmarkBigcacheSet-12 4107825 268.9 ns/op 55 B/op 0 allocs/op 141 | BenchmarkFreecacheSet-12 44181687 28.4 ns/op 0 B/op 0 allocs/op 142 | BenchmarkGoCacheSet-12 4921483 249.0 ns/op 16 B/op 1 allocs/op 143 | ``` 144 | 145 | > Notice: Ecache only has lru mode, including v1 and v2; Freecache has 256 shardings, and we can't reset to 1. 146 | 147 | > Benchmarks: [_examples/performance_test.go](./_examples/performance_test.go). 148 | 149 | ### 👥 Contributors 150 | 151 | * [cristiane](https://gitee.com/cristiane): Provide some optimizations about hash 152 | * [hzy15610046011](https://gitee.com/hzy15610046011): Provide architecture design documents and pictures 153 | * [chen661](https://gitee.com/chen661): Provide the limit thought of argument in WithSegmentSize Option 154 | 155 | Please open an _**issue**_ if you find something is not working as expected. 156 | 157 | [![Star History Chart](https://api.star-history.com/svg?repos=fishgoddess/cachego&type=Date)](https://star-history.com/#fishgoddess/cachego&Date) 158 | -------------------------------------------------------------------------------- /option_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cachego 16 | 17 | import ( 18 | "testing" 19 | "time" 20 | ) 21 | 22 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithCacheName$ 23 | func TestWithCacheName(t *testing.T) { 24 | got := &config{cacheName: ""} 25 | expect := &config{cacheName: "-"} 26 | 27 | WithCacheName("-").applyTo(got) 28 | if !isConfigEquals(got, expect) { 29 | t.Fatalf("got %+v != expect %+v", got, expect) 30 | } 31 | } 32 | 33 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithLRU$ 34 | func TestWithLRU(t *testing.T) { 35 | got := &config{cacheType: standard, maxEntries: 0} 36 | expect := &config{cacheType: lru, maxEntries: 666} 37 | 38 | WithLRU(666).applyTo(got) 39 | if !isConfigEquals(got, expect) { 40 | t.Fatalf("got %+v != expect %+v", got, expect) 41 | } 42 | } 43 | 44 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithLFU$ 45 | func TestWithLFU(t *testing.T) { 46 | got := &config{cacheType: standard, maxEntries: 0} 47 | expect := &config{cacheType: lfu, maxEntries: 999} 48 | 49 | WithLFU(999).applyTo(got) 50 | if !isConfigEquals(got, expect) { 51 | t.Fatalf("got %+v != expect %+v", got, expect) 52 | } 53 | } 54 | 55 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithShardings$ 56 | func TestWithShardings(t *testing.T) { 57 | got := &config{shardings: 0} 58 | expect := &config{shardings: 1024} 59 | 60 | WithShardings(1024).applyTo(got) 61 | if !isConfigEquals(got, expect) { 62 | t.Fatalf("got %+v != expect %+v", got, expect) 63 | } 64 | } 65 | 66 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithDisableSingleflight$ 67 | func TestWithDisableSingleflight(t *testing.T) { 68 | got := &config{singleflight: true} 69 | expect := &config{singleflight: false} 70 | 71 | WithDisableSingleflight().applyTo(got) 72 | if !isConfigEquals(got, expect) { 73 | t.Fatalf("got %+v != expect %+v", got, expect) 74 | } 75 | } 76 | 77 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithGC$ 78 | func TestWithGC(t *testing.T) { 79 | got := &config{gcDuration: 0} 80 | expect := &config{gcDuration: 1024} 81 | 82 | WithGC(1024).applyTo(got) 83 | if !isConfigEquals(got, expect) { 84 | t.Fatalf("got %+v != expect %+v", got, expect) 85 | } 86 | } 87 | 88 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithMaxScans$ 89 | func TestWithMaxScans(t *testing.T) { 90 | got := &config{maxScans: 0} 91 | expect := &config{maxScans: 1024} 92 | 93 | WithMaxScans(1024).applyTo(got) 94 | if !isConfigEquals(got, expect) { 95 | t.Fatalf("got %+v != expect %+v", got, expect) 96 | } 97 | } 98 | 99 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithMaxEntries$ 100 | func TestWithMaxEntries(t *testing.T) { 101 | got := &config{maxEntries: 0} 102 | expect := &config{maxEntries: 1024} 103 | 104 | WithMaxEntries(1024).applyTo(got) 105 | if !isConfigEquals(got, expect) { 106 | t.Fatalf("got %+v != expect %+v", got, expect) 107 | } 108 | } 109 | 110 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithNow$ 111 | func TestWithNow(t *testing.T) { 112 | now := func() int64 { 113 | return 0 114 | } 115 | 116 | got := &config{now: nil} 117 | expect := &config{now: now} 118 | 119 | WithNow(now).applyTo(got) 120 | if !isConfigEquals(got, expect) { 121 | t.Fatalf("got %+v != expect %+v", got, expect) 122 | } 123 | } 124 | 125 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithHash$ 126 | func TestWithHash(t *testing.T) { 127 | hash := func(key string) int { 128 | return 0 129 | } 130 | 131 | got := &config{hash: nil} 132 | expect := &config{hash: hash} 133 | 134 | WithHash(hash).applyTo(got) 135 | if !isConfigEquals(got, expect) { 136 | t.Fatalf("got %+v != expect %+v", got, expect) 137 | } 138 | } 139 | 140 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithRecordMissed$ 141 | func TestWithRecordMissed(t *testing.T) { 142 | got := &config{recordMissed: false} 143 | expect := &config{recordMissed: true} 144 | 145 | WithRecordMissed(true).applyTo(got) 146 | if !isConfigEquals(got, expect) { 147 | t.Fatalf("got %+v != expect %+v", got, expect) 148 | } 149 | } 150 | 151 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithRecordHit$ 152 | func TestWithRecordHit(t *testing.T) { 153 | got := &config{recordHit: false} 154 | expect := &config{recordHit: true} 155 | 156 | WithRecordHit(true).applyTo(got) 157 | if !isConfigEquals(got, expect) { 158 | t.Fatalf("got %+v != expect %+v", got, expect) 159 | } 160 | } 161 | 162 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithRecordGC$ 163 | func TestWithRecordGC(t *testing.T) { 164 | got := &config{recordGC: false} 165 | expect := &config{recordGC: true} 166 | 167 | WithRecordGC(true).applyTo(got) 168 | if !isConfigEquals(got, expect) { 169 | t.Fatalf("got %+v != expect %+v", got, expect) 170 | } 171 | } 172 | 173 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithRecordLoad$ 174 | func TestWithRecordLoad(t *testing.T) { 175 | got := &config{recordLoad: false} 176 | expect := &config{recordLoad: true} 177 | 178 | WithRecordLoad(true).applyTo(got) 179 | if !isConfigEquals(got, expect) { 180 | t.Fatalf("got %+v != expect %+v", got, expect) 181 | } 182 | } 183 | 184 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithReportMissed$ 185 | func TestWithReportMissed(t *testing.T) { 186 | reportMissed := func(reporter *Reporter, key string) {} 187 | 188 | got := &config{reportMissed: nil} 189 | expect := &config{reportMissed: reportMissed} 190 | 191 | WithReportMissed(reportMissed).applyTo(got) 192 | if !isConfigEquals(got, expect) { 193 | t.Fatalf("got %+v != expect %+v", got, expect) 194 | } 195 | } 196 | 197 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithReportHit$ 198 | func TestWithReportHit(t *testing.T) { 199 | reportHit := func(reporter *Reporter, key string, value interface{}) {} 200 | 201 | got := &config{reportHit: nil} 202 | expect := &config{reportHit: reportHit} 203 | 204 | WithReportHit(reportHit).applyTo(got) 205 | if !isConfigEquals(got, expect) { 206 | t.Fatalf("got %+v != expect %+v", got, expect) 207 | } 208 | } 209 | 210 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithReportGC$ 211 | func TestWithReportGC(t *testing.T) { 212 | reportGC := func(reporter *Reporter, cost time.Duration, cleans int) {} 213 | 214 | got := &config{reportGC: nil} 215 | expect := &config{reportGC: reportGC} 216 | 217 | WithReportGC(reportGC).applyTo(got) 218 | if !isConfigEquals(got, expect) { 219 | t.Fatalf("got %+v != expect %+v", got, expect) 220 | } 221 | } 222 | 223 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestWithReportLoad$ 224 | func TestWithReportLoad(t *testing.T) { 225 | reportLoad := func(reporter *Reporter, key string, value interface{}, ttl time.Duration, err error) {} 226 | 227 | got := &config{reportLoad: nil} 228 | expect := &config{reportLoad: reportLoad} 229 | 230 | WithReportLoad(reportLoad).applyTo(got) 231 | if !isConfigEquals(got, expect) { 232 | t.Fatalf("got %+v != expect %+v", got, expect) 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /report_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cachego 16 | 17 | import ( 18 | "io" 19 | "testing" 20 | "time" 21 | ) 22 | 23 | const ( 24 | testCacheName = "test" 25 | testCacheType = lru 26 | testCacheShardings = 16 27 | testCacheGCDuration = 10 * time.Minute 28 | ) 29 | 30 | func newTestReportableCache() (*reportableCache, *Reporter) { 31 | conf := newDefaultConfig() 32 | conf.cacheName = testCacheName 33 | conf.cacheType = testCacheType 34 | conf.shardings = testCacheShardings 35 | conf.gcDuration = testCacheGCDuration 36 | conf.maxEntries = maxTestEntries 37 | 38 | cache, reporter := report(conf, newStandardCache(conf)) 39 | return cache.(*reportableCache), reporter 40 | } 41 | 42 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestReportableCache$ 43 | func TestReportableCache(t *testing.T) { 44 | cache, _ := newTestReportableCache() 45 | testCacheImplement(t, cache) 46 | } 47 | 48 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestReportableCacheReportMissed$ 49 | func TestReportableCacheReportMissed(t *testing.T) { 50 | cache, reporter := newTestReportableCache() 51 | cache.Set("key", 666, NoTTL) 52 | 53 | checked := false 54 | cache.reportMissed = func(reporter *Reporter, key string) { 55 | if key == "key" { 56 | t.Fatal("key == \"key\"") 57 | } 58 | 59 | if key != "missed" { 60 | t.Fatalf("key %s != \"missed\"", key) 61 | } 62 | 63 | checked = true 64 | } 65 | 66 | cache.Get("key") 67 | cache.Get("missed") 68 | 69 | if !checked { 70 | t.Fatal("reportMissed not checked") 71 | } 72 | 73 | if reporter.CountMissed() != 1 { 74 | t.Fatalf("CountMissed %d is wrong", reporter.CountMissed()) 75 | } 76 | 77 | missedRate := reporter.MissedRate() 78 | if missedRate < 0.499 || missedRate > 0.501 { 79 | t.Fatalf("missedRate %.3f is wrong", missedRate) 80 | } 81 | } 82 | 83 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestReportableCacheReportHit$ 84 | func TestReportableCacheReportHit(t *testing.T) { 85 | cache, reporter := newTestReportableCache() 86 | cache.Set("key", 666, NoTTL) 87 | 88 | checked := false 89 | cache.reportHit = func(reporter *Reporter, key string, value interface{}) { 90 | if key == "missed" { 91 | t.Fatal("key == \"missed\"") 92 | } 93 | 94 | if key != "key" { 95 | t.Fatalf("key %s != \"key\"", key) 96 | } 97 | 98 | if value.(int) != 666 { 99 | t.Fatalf("value.(int) %d is wrong", value.(int)) 100 | } 101 | 102 | checked = true 103 | } 104 | 105 | cache.Get("key") 106 | cache.Get("missed") 107 | 108 | if !checked { 109 | t.Fatal("reportHit not checked") 110 | } 111 | 112 | if reporter.CountHit() != 1 { 113 | t.Fatalf("CountHit %d is wrong", reporter.CountHit()) 114 | } 115 | 116 | hitRate := reporter.HitRate() 117 | if hitRate < 0.499 || hitRate > 0.501 { 118 | t.Fatalf("hitRate %.3f is wrong", hitRate) 119 | } 120 | } 121 | 122 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestReportableCacheReportGC$ 123 | func TestReportableCacheReportGC(t *testing.T) { 124 | cache, reporter := newTestReportableCache() 125 | cache.Set("key1", 1, time.Millisecond) 126 | cache.Set("key2", 2, time.Millisecond) 127 | cache.Set("key3", 3, time.Millisecond) 128 | cache.Set("key4", 4, time.Second) 129 | cache.Set("key5", 5, time.Second) 130 | 131 | gcCount := uint64(0) 132 | checked := false 133 | 134 | cache.reportGC = func(reporter *Reporter, cost time.Duration, cleans int) { 135 | if cost <= 0 { 136 | t.Fatalf("cost %d <= 0", cost) 137 | } 138 | 139 | if cleans != 3 { 140 | t.Fatalf("cleans %d is wrong", cleans) 141 | } 142 | 143 | gcCount++ 144 | checked = true 145 | } 146 | 147 | time.Sleep(10 * time.Millisecond) 148 | 149 | cleans := cache.GC() 150 | if cleans != 3 { 151 | t.Fatalf("cleans %d is wrong", cleans) 152 | } 153 | 154 | if !checked { 155 | t.Fatal("reportHit not checked") 156 | } 157 | 158 | if reporter.CountGC() != gcCount { 159 | t.Fatalf("CountGC %d is wrong", reporter.CountGC()) 160 | } 161 | } 162 | 163 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestReportableCacheReportLoad$ 164 | func TestReportableCacheReportLoad(t *testing.T) { 165 | cache, reporter := newTestReportableCache() 166 | 167 | loadCount := uint64(0) 168 | checked := false 169 | 170 | cache.reportLoad = func(reporter *Reporter, key string, value interface{}, ttl time.Duration, err error) { 171 | if key != "load" { 172 | t.Fatalf("key %s is wrong", key) 173 | } 174 | 175 | if value.(int) != 999 { 176 | t.Fatalf("value.(int) %d is wrong", value.(int)) 177 | } 178 | 179 | if ttl != time.Second { 180 | t.Fatalf("ttl %s is wrong", ttl) 181 | } 182 | 183 | if err != io.EOF { 184 | t.Fatalf("err %+v is wrong", err) 185 | } 186 | 187 | loadCount++ 188 | checked = true 189 | } 190 | 191 | value, err := cache.Load("load", time.Second, func() (value interface{}, err error) { 192 | return 999, io.EOF 193 | }) 194 | 195 | if value.(int) != 999 { 196 | t.Fatalf("value.(int) %d is wrong", value.(int)) 197 | } 198 | 199 | if err != io.EOF { 200 | t.Fatalf("err %+v is wrong", err) 201 | } 202 | 203 | if !checked { 204 | t.Fatal("reportLoad not checked") 205 | } 206 | 207 | if reporter.CountLoad() != loadCount { 208 | t.Fatalf("CountLoad %d is wrong", reporter.CountLoad()) 209 | } 210 | } 211 | 212 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestReporterCacheName$ 213 | func TestReporterCacheName(t *testing.T) { 214 | _, reporter := newTestReportableCache() 215 | if reporter.CacheName() != reporter.conf.cacheName { 216 | t.Fatalf("CacheName %s is wrong compared with conf", reporter.CacheName()) 217 | } 218 | 219 | if reporter.CacheName() != testCacheName { 220 | t.Fatalf("CacheName %s is wrong", reporter.CacheName()) 221 | } 222 | } 223 | 224 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestReporterCacheType$ 225 | func TestReporterCacheType(t *testing.T) { 226 | _, reporter := newTestReportableCache() 227 | if reporter.CacheType() != reporter.conf.cacheType { 228 | t.Fatalf("CacheType %s is wrong compared with conf", reporter.CacheType()) 229 | } 230 | 231 | if reporter.CacheType() != testCacheType { 232 | t.Fatalf("CacheType %s is wrong", reporter.CacheType()) 233 | } 234 | } 235 | 236 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestReporterCacheShardings$ 237 | func TestReporterCacheShardings(t *testing.T) { 238 | _, reporter := newTestReportableCache() 239 | if reporter.CacheShardings() != reporter.conf.shardings { 240 | t.Fatalf("CacheShardings %d is wrong compared with conf", reporter.CacheShardings()) 241 | } 242 | 243 | if reporter.CacheShardings() != testCacheShardings { 244 | t.Fatalf("CacheShardings %d is wrong", reporter.CacheShardings()) 245 | } 246 | } 247 | 248 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestReporterCacheGC$ 249 | func TestReporterCacheGC(t *testing.T) { 250 | _, reporter := newTestReportableCache() 251 | if reporter.CacheGC() != reporter.conf.gcDuration { 252 | t.Fatalf("CacheGC %d is wrong compared with conf", reporter.CacheGC()) 253 | } 254 | 255 | if reporter.CacheGC() != testCacheGCDuration { 256 | t.Fatalf("CacheGC %d is wrong", reporter.CacheGC()) 257 | } 258 | } 259 | 260 | // go test -v -cover -count=1 -test.cpu=1 -run=^TestReporterCacheSize$ 261 | func TestReporterCacheSize(t *testing.T) { 262 | cache, reporter := newTestReportableCache() 263 | cache.Set("key1", 1, time.Millisecond) 264 | cache.Set("key2", 2, time.Millisecond) 265 | cache.Set("key3", 3, time.Millisecond) 266 | cache.Set("key4", 4, time.Second) 267 | cache.Set("key5", 5, time.Second) 268 | 269 | if reporter.CacheSize() != 5 { 270 | t.Fatalf("CacheSize %d is wrong", reporter.CacheSize()) 271 | } 272 | 273 | time.Sleep(100 * time.Millisecond) 274 | cache.GC() 275 | 276 | if reporter.CacheSize() != 2 { 277 | t.Fatalf("CacheSize %d is wrong", reporter.CacheSize()) 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /cache_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cachego 16 | 17 | import ( 18 | "strconv" 19 | "sync/atomic" 20 | "testing" 21 | "time" 22 | ) 23 | 24 | const ( 25 | maxTestEntries = 10 26 | ) 27 | 28 | type testCache struct { 29 | *config 30 | loader *loader 31 | 32 | count int32 33 | } 34 | 35 | func (tc *testCache) currentCount() int32 { 36 | return atomic.LoadInt32(&tc.count) 37 | } 38 | 39 | func (tc *testCache) Get(key string) (value interface{}, found bool) { 40 | return nil, false 41 | } 42 | 43 | func (tc *testCache) Set(key string, value interface{}, ttl time.Duration) (evictedValue interface{}) { 44 | return nil 45 | } 46 | 47 | func (tc *testCache) Remove(key string) (removedValue interface{}) { 48 | return nil 49 | } 50 | 51 | func (tc *testCache) Size() (size int) { 52 | return 0 53 | } 54 | 55 | func (tc *testCache) GC() (cleans int) { 56 | atomic.AddInt32(&tc.count, 1) 57 | return 0 58 | } 59 | 60 | func (tc *testCache) Reset() {} 61 | 62 | func (tc *testCache) Load(key string, ttl time.Duration, load func() (value interface{}, err error)) (value interface{}, err error) { 63 | return nil, nil 64 | } 65 | 66 | func testCacheGet(t *testing.T, cache Cache) { 67 | value, found := cache.Get("key") 68 | if found { 69 | t.Fatalf("get %+v should be not found", value) 70 | } 71 | 72 | cache.Set("key", "value", time.Millisecond) 73 | 74 | value, found = cache.Get("key") 75 | if !found { 76 | t.Fatal("get should be found") 77 | } 78 | 79 | if value.(string) != "value" { 80 | t.Fatalf("value %+v is wrong", value) 81 | } 82 | 83 | time.Sleep(2 * time.Millisecond) 84 | 85 | value, found = cache.Get("key") 86 | if found { 87 | t.Fatalf("get %+v should be not found", value) 88 | } 89 | } 90 | 91 | func testCacheSet(t *testing.T, cache Cache) { 92 | value, found := cache.Get("key") 93 | if found { 94 | t.Fatalf("get %+v should be not found", value) 95 | } 96 | 97 | cache.Set("key", "value", time.Millisecond) 98 | 99 | value, found = cache.Get("key") 100 | if !found { 101 | t.Fatal("get should be found") 102 | } 103 | 104 | if value.(string) != "value" { 105 | t.Fatalf("value %+v is wrong", value) 106 | } 107 | 108 | time.Sleep(2 * time.Millisecond) 109 | 110 | value, found = cache.Get("key") 111 | if found { 112 | t.Fatalf("get %+v should be not found", value) 113 | } 114 | 115 | cache.Set("key", "value", NoTTL) 116 | 117 | value, found = cache.Get("key") 118 | if !found { 119 | t.Fatal("get should be found") 120 | } 121 | 122 | if value.(string) != "value" { 123 | t.Fatalf("value %+v is wrong", value) 124 | } 125 | 126 | time.Sleep(2 * time.Millisecond) 127 | 128 | value, found = cache.Get("key") 129 | if !found { 130 | t.Fatal("get should be found") 131 | } 132 | } 133 | 134 | func testCacheRemove(t *testing.T, cache Cache) { 135 | removedValue := cache.Remove("key") 136 | if removedValue != nil { 137 | t.Fatalf("removedValue %+v is wrong", removedValue) 138 | } 139 | 140 | cache.Set("key", "value", NoTTL) 141 | 142 | removedValue = cache.Remove("key") 143 | if removedValue.(string) != "value" { 144 | t.Fatalf("removedValue %+v is wrong", removedValue) 145 | } 146 | 147 | cache.Set("key", "value", time.Millisecond) 148 | time.Sleep(2 * time.Millisecond) 149 | 150 | removedValue = cache.Remove("key") 151 | if removedValue == nil { 152 | t.Fatal("removedValue == nil") 153 | } 154 | } 155 | 156 | func testCacheSize(t *testing.T, cache Cache) { 157 | size := cache.Size() 158 | if size != 0 { 159 | t.Fatalf("size %d is wrong", size) 160 | } 161 | 162 | for i := int64(0); i < maxTestEntries; i++ { 163 | cache.Set(strconv.FormatInt(i, 10), i, NoTTL) 164 | } 165 | 166 | size = cache.Size() 167 | if size != maxTestEntries { 168 | t.Fatalf("size %d is wrong", size) 169 | } 170 | } 171 | 172 | func testCacheGC(t *testing.T, cache Cache) { 173 | size := cache.Size() 174 | if size != 0 { 175 | t.Fatalf("size %d is wrong", size) 176 | } 177 | 178 | for i := int64(0); i < maxTestEntries; i++ { 179 | if i&1 == 0 { 180 | cache.Set(strconv.FormatInt(i, 10), i, NoTTL) 181 | } else { 182 | cache.Set(strconv.FormatInt(i, 10), i, time.Millisecond) 183 | } 184 | } 185 | 186 | size = cache.Size() 187 | if size != maxTestEntries { 188 | t.Fatalf("size %d is wrong", size) 189 | } 190 | 191 | cache.GC() 192 | 193 | size = cache.Size() 194 | if size != maxTestEntries { 195 | t.Fatalf("size %d is wrong", size) 196 | } 197 | 198 | time.Sleep(2 * time.Millisecond) 199 | 200 | cache.GC() 201 | 202 | size = cache.Size() 203 | if size != maxTestEntries/2 { 204 | t.Fatalf("size %d is wrong", size) 205 | } 206 | } 207 | 208 | func testCacheReset(t *testing.T, cache Cache) { 209 | for i := int64(0); i < maxTestEntries; i++ { 210 | cache.Set(strconv.FormatInt(i, 10), i, NoTTL) 211 | } 212 | 213 | for i := int64(0); i < maxTestEntries; i++ { 214 | value, found := cache.Get(strconv.FormatInt(i, 10)) 215 | if !found { 216 | t.Fatalf("get %d should be found", i) 217 | } 218 | 219 | if value.(int64) != i { 220 | t.Fatalf("value %+v is wrong", value) 221 | } 222 | } 223 | 224 | size := cache.Size() 225 | if size != maxTestEntries { 226 | t.Fatalf("size %d is wrong", size) 227 | } 228 | 229 | cache.Reset() 230 | 231 | for i := int64(0); i < maxTestEntries; i++ { 232 | value, found := cache.Get(strconv.FormatInt(i, 10)) 233 | if found { 234 | t.Fatalf("get %d, %+v should be not found", i, value) 235 | } 236 | } 237 | 238 | size = cache.Size() 239 | if size != 0 { 240 | t.Fatalf("size %d is wrong", size) 241 | } 242 | } 243 | 244 | func testCacheImplement(t *testing.T, cache Cache) { 245 | testCaches := []func(t *testing.T, cache Cache){ 246 | testCacheGet, testCacheSet, testCacheRemove, testCacheSize, testCacheGC, testCacheReset, 247 | } 248 | 249 | for _, testCache := range testCaches { 250 | cache.Reset() 251 | testCache(t, cache) 252 | } 253 | } 254 | 255 | // go test -v -cover -count=1 -test.cpu=1=^TestNewCache$ 256 | func TestNewCache(t *testing.T) { 257 | cache := NewCache() 258 | 259 | sc1, ok := cache.(*standardCache) 260 | if !ok { 261 | t.Fatalf("cache.(*standardCache) %T not ok", cache) 262 | } 263 | 264 | if sc1 == nil { 265 | t.Fatal("sc1 == nil") 266 | } 267 | 268 | cache = NewCache(WithLRU(16)) 269 | 270 | sc2, ok := cache.(*lruCache) 271 | if !ok { 272 | t.Fatalf("cache.(*lruCache) %T not ok", cache) 273 | } 274 | 275 | if sc2 == nil { 276 | t.Fatal("sc2 == nil") 277 | } 278 | 279 | cache = NewCache(WithShardings(64)) 280 | 281 | sc, ok := cache.(*shardingCache) 282 | if !ok { 283 | t.Fatalf("cache.(*shardingCache) %T not ok", cache) 284 | } 285 | 286 | if sc == nil { 287 | t.Fatal("sc == nil") 288 | } 289 | 290 | defer func() { 291 | if r := recover(); r == nil { 292 | t.Fatal("new should panic") 293 | } 294 | }() 295 | 296 | cache = NewCache(WithLRU(0)) 297 | } 298 | 299 | // go test -v -cover -count=1 -test.cpu=1=^TestNewCacheWithReport$ 300 | func TestNewCacheWithReport(t *testing.T) { 301 | cache, reporter := NewCacheWithReport() 302 | 303 | sc1, ok := cache.(*reportableCache) 304 | if !ok { 305 | t.Fatalf("cache.(*reportableCache) %T not ok", cache) 306 | } 307 | 308 | if sc1 == nil { 309 | t.Fatal("sc1 == nil") 310 | } 311 | 312 | if reporter == nil { 313 | t.Fatal("reporter == nil") 314 | } 315 | } 316 | 317 | // go test -v -cover -count=1 -test.cpu=1=^TestRunGCTask$ 318 | func TestRunGCTask(t *testing.T) { 319 | cache := new(testCache) 320 | 321 | count := cache.currentCount() 322 | if count != 0 { 323 | t.Fatalf("cache.currentCount() %d is wrong", count) 324 | } 325 | 326 | cancel := RunGCTask(cache, 10*time.Millisecond) 327 | 328 | time.Sleep(105 * time.Millisecond) 329 | 330 | count = cache.currentCount() 331 | if count != 10 { 332 | t.Fatalf("cache.currentCount() %d is wrong", count) 333 | } 334 | 335 | time.Sleep(80 * time.Millisecond) 336 | cancel() 337 | 338 | count = cache.currentCount() 339 | if count != 18 { 340 | t.Fatalf("cache.currentCount() %d is wrong", count) 341 | } 342 | 343 | time.Sleep(time.Second) 344 | 345 | count = cache.currentCount() 346 | if count != 18 { 347 | t.Fatalf("cache.currentCount() %d is wrong", count) 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /_examples/performance_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FishGoddess. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "math/rand" 19 | "strconv" 20 | "testing" 21 | "time" 22 | 23 | "github.com/FishGoddess/cachego" 24 | ) 25 | 26 | const ( 27 | benchTTL = time.Minute 28 | benchMaxKeys = 10000 29 | benchMaxEntries = 100000 30 | ) 31 | 32 | type benchKeys []string 33 | 34 | func newBenchKeys() benchKeys { 35 | keys := make([]string, 0, benchMaxKeys) 36 | 37 | for i := 0; i < benchMaxKeys; i++ { 38 | keys = append(keys, strconv.Itoa(i)) 39 | } 40 | 41 | return keys 42 | } 43 | 44 | func (bks benchKeys) pick() string { 45 | index := rand.Intn(len(bks)) 46 | return bks[index] 47 | } 48 | 49 | func (bks benchKeys) loop(fn func(key string)) { 50 | for _, key := range bks { 51 | fn(key) 52 | } 53 | } 54 | 55 | func benchmarkCacheGet(b *testing.B, set func(key string, value string), get func(key string)) { 56 | keys := newBenchKeys() 57 | keys.loop(func(key string) { 58 | set(key, key) 59 | }) 60 | 61 | b.ReportAllocs() 62 | b.ResetTimer() 63 | 64 | b.RunParallel(func(pb *testing.PB) { 65 | key := keys.pick() 66 | 67 | for pb.Next() { 68 | get(key) 69 | } 70 | }) 71 | } 72 | 73 | func benchmarkCacheSet(b *testing.B, set func(key string, value string)) { 74 | keys := newBenchKeys() 75 | 76 | b.ReportAllocs() 77 | b.ResetTimer() 78 | 79 | b.RunParallel(func(pb *testing.PB) { 80 | key := keys.pick() 81 | 82 | for pb.Next() { 83 | set(key, key) 84 | } 85 | }) 86 | } 87 | 88 | // go test -v -bench=^BenchmarkCachegoGet$ -benchtime=1s ./_examples/performance_test.go 89 | func BenchmarkCachegoGet(b *testing.B) { 90 | cache := cachego.NewCache() 91 | 92 | set := func(key string, value string) { 93 | cache.Set(key, value, benchTTL) 94 | } 95 | 96 | get := func(key string) { 97 | cache.Get(key) 98 | } 99 | 100 | benchmarkCacheGet(b, set, get) 101 | } 102 | 103 | // go test -v -bench=^BenchmarkCachegoGetLRU$ -benchtime=1s ./_examples/performance_test.go 104 | func BenchmarkCachegoGetLRU(b *testing.B) { 105 | cache := cachego.NewCache(cachego.WithLRU(benchMaxEntries)) 106 | 107 | set := func(key string, value string) { 108 | cache.Set(key, value, benchTTL) 109 | } 110 | 111 | get := func(key string) { 112 | cache.Get(key) 113 | } 114 | 115 | benchmarkCacheGet(b, set, get) 116 | } 117 | 118 | // go test -v -bench=^BenchmarkCachegoGetLFU$ -benchtime=1s ./_examples/performance_test.go 119 | func BenchmarkCachegoGetLFU(b *testing.B) { 120 | cache := cachego.NewCache(cachego.WithLFU(benchMaxEntries)) 121 | 122 | set := func(key string, value string) { 123 | cache.Set(key, value, benchTTL) 124 | } 125 | 126 | get := func(key string) { 127 | cache.Get(key) 128 | } 129 | 130 | benchmarkCacheGet(b, set, get) 131 | } 132 | 133 | // go test -v -bench=^BenchmarkCachegoGetSharding$ -benchtime=1s ./_examples/performance_test.go 134 | func BenchmarkCachegoGetSharding(b *testing.B) { 135 | cache := cachego.NewCache(cachego.WithShardings(16)) 136 | 137 | set := func(key string, value string) { 138 | cache.Set(key, value, benchTTL) 139 | } 140 | 141 | get := func(key string) { 142 | cache.Get(key) 143 | } 144 | 145 | benchmarkCacheGet(b, set, get) 146 | } 147 | 148 | // go test -v -bench=^BenchmarkCachegoSet$ -benchtime=1s ./_examples/performance_test.go 149 | func BenchmarkCachegoSet(b *testing.B) { 150 | cache := cachego.NewCache() 151 | 152 | benchmarkCacheSet(b, func(key string, value string) { 153 | cache.Set(key, value, benchTTL) 154 | }) 155 | } 156 | 157 | // go test -v -bench=^BenchmarkCachegoSetLRU$ -benchtime=1s ./_examples/performance_test.go 158 | func BenchmarkCachegoSetLRU(b *testing.B) { 159 | cache := cachego.NewCache(cachego.WithLRU(benchMaxEntries)) 160 | 161 | benchmarkCacheSet(b, func(key string, value string) { 162 | cache.Set(key, value, benchTTL) 163 | }) 164 | } 165 | 166 | // go test -v -bench=^BenchmarkCachegoSetLFU$ -benchtime=1s ./_examples/performance_test.go 167 | func BenchmarkCachegoSetLFU(b *testing.B) { 168 | cache := cachego.NewCache(cachego.WithLFU(benchMaxEntries)) 169 | 170 | benchmarkCacheSet(b, func(key string, value string) { 171 | cache.Set(key, value, benchTTL) 172 | }) 173 | } 174 | 175 | // go test -v -bench=^BenchmarkCachegoSetSharding$ -benchtime=1s ./_examples/performance_test.go 176 | func BenchmarkCachegoSetSharding(b *testing.B) { 177 | cache := cachego.NewCache(cachego.WithShardings(16)) 178 | 179 | benchmarkCacheSet(b, func(key string, value string) { 180 | cache.Set(key, value, benchTTL) 181 | }) 182 | } 183 | 184 | //// go test -v -bench=^BenchmarkGcacheGet$ -benchtime=1s ./_examples/performance_test.go 185 | //func BenchmarkGcacheGet(b *testing.B) { 186 | // cache := gcache.New(benchMaxEntries).Expiration(benchTTL).Build() 187 | // 188 | // set := func(key string, value string) { 189 | // cache.Set(key, value) 190 | // } 191 | // 192 | // get := func(key string) { 193 | // cache.Get(key) 194 | // } 195 | // 196 | // benchmarkCacheGet(b, set, get) 197 | //} 198 | // 199 | //// go test -v -bench=^BenchmarkGcacheGetLRU$ -benchtime=1s ./_examples/performance_test.go 200 | //func BenchmarkGcacheGetLRU(b *testing.B) { 201 | // cache := gcache.New(benchMaxEntries).Expiration(benchTTL).LRU().Build() 202 | // 203 | // set := func(key string, value string) { 204 | // cache.Set(key, value) 205 | // } 206 | // 207 | // get := func(key string) { 208 | // cache.Get(key) 209 | // } 210 | // 211 | // benchmarkCacheGet(b, set, get) 212 | //} 213 | // 214 | //// go test -v -bench=^BenchmarkGcacheGetLFU$ -benchtime=1s ./_examples/performance_test.go 215 | //func BenchmarkGcacheGetLFU(b *testing.B) { 216 | // cache := gcache.New(benchMaxEntries).Expiration(benchTTL).LFU().Build() 217 | // 218 | // set := func(key string, value string) { 219 | // cache.Set(key, value) 220 | // } 221 | // 222 | // get := func(key string) { 223 | // cache.Get(key) 224 | // } 225 | // 226 | // benchmarkCacheGet(b, set, get) 227 | //} 228 | // 229 | //// go test -v -bench=^BenchmarkEcacheGet$ -benchtime=1s ./_examples/performance_test.go 230 | //func BenchmarkEcacheGet(b *testing.B) { 231 | // cache := ecache.NewLRUCache(1, math.MaxUint16, benchTTL) 232 | // 233 | // set := func(key string, value string) { 234 | // cache.Put(key, value) 235 | // } 236 | // 237 | // get := func(key string) { 238 | // cache.Get(key) 239 | // } 240 | // 241 | // benchmarkCacheGet(b, set, get) 242 | //} 243 | // 244 | //// go test -v -bench=^BenchmarkEcache2Get$ -benchtime=1s ./_examples/performance_test.go 245 | //func BenchmarkEcache2Get(b *testing.B) { 246 | // cache := ecache.NewLRUCache(1, math.MaxUint16, benchTTL).LRU2(16) 247 | // 248 | // set := func(key string, value string) { 249 | // cache.Put(key, value) 250 | // } 251 | // 252 | // get := func(key string) { 253 | // cache.Get(key) 254 | // } 255 | // 256 | // benchmarkCacheGet(b, set, get) 257 | //} 258 | // 259 | //// go test -v -bench=^BenchmarkBigcacheGet$ -benchtime=1s ./_examples/performance_test.go 260 | //func BenchmarkBigcacheGet(b *testing.B) { 261 | // cache, _ := bigcache.New(context.Background(), bigcache.Config{ 262 | // Shards: 1, 263 | // LifeWindow: benchTTL, 264 | // MaxEntriesInWindow: benchMaxEntries, 265 | // Verbose: false, 266 | // }) 267 | // 268 | // set := func(key string, value string) { 269 | // cache.Set(key, []byte(value)) 270 | // } 271 | // 272 | // get := func(key string) { 273 | // cache.Get(key) 274 | // } 275 | // 276 | // benchmarkCacheGet(b, set, get) 277 | //} 278 | // 279 | //// go test -v -bench=^BenchmarkFreecacheGet$ -benchtime=1s ./_examples/performance_test.go 280 | //func BenchmarkFreecacheGet(b *testing.B) { 281 | // cache := freecache.NewCache(benchMaxEntries) 282 | // 283 | // set := func(key string, value string) { 284 | // cache.Set([]byte(key), []byte(value), int(benchTTL.Seconds())) 285 | // } 286 | // 287 | // get := func(key string) { 288 | // cache.Get([]byte(key)) 289 | // } 290 | // 291 | // benchmarkCacheGet(b, set, get) 292 | //} 293 | // 294 | //// go test -v -bench=^BenchmarkGoCacheGet$ -benchtime=1s ./_examples/performance_test.go 295 | //func BenchmarkGoCacheGet(b *testing.B) { 296 | // cache := gocache.New(benchTTL, 0) 297 | // 298 | // set := func(key string, value string) { 299 | // cache.Set(key, value, benchTTL) 300 | // } 301 | // 302 | // get := func(key string) { 303 | // cache.Get(key) 304 | // } 305 | // 306 | // benchmarkCacheGet(b, set, get) 307 | //} 308 | // 309 | //// go test -v -bench=^BenchmarkGcacheSet$ -benchtime=1s ./_examples/performance_test.go 310 | //func BenchmarkGcacheSet(b *testing.B) { 311 | // cache := gcache.New(benchMaxEntries).Expiration(benchTTL).Build() 312 | // 313 | // benchmarkCacheSet(b, func(key string, value string) { 314 | // cache.Set(key, value) 315 | // }) 316 | //} 317 | // 318 | //// go test -v -bench=^BenchmarkGcacheSetLRU$ -benchtime=1s ./_examples/performance_test.go 319 | //func BenchmarkGcacheSetLRU(b *testing.B) { 320 | // cache := gcache.New(benchMaxEntries).Expiration(benchTTL).LRU().Build() 321 | // 322 | // benchmarkCacheSet(b, func(key string, value string) { 323 | // cache.Set(key, value) 324 | // }) 325 | //} 326 | // 327 | //// go test -v -bench=^BenchmarkGcacheSetLFU$ -benchtime=1s ./_examples/performance_test.go 328 | //func BenchmarkGcacheSetLFU(b *testing.B) { 329 | // cache := gcache.New(benchMaxEntries).Expiration(benchTTL).LFU().Build() 330 | // 331 | // benchmarkCacheSet(b, func(key string, value string) { 332 | // cache.Set(key, value) 333 | // }) 334 | //} 335 | // 336 | //// go test -v -bench=^BenchmarkEcacheSet$ -benchtime=1s ./_examples/performance_test.go 337 | //func BenchmarkEcacheSet(b *testing.B) { 338 | // cache := ecache.NewLRUCache(1, math.MaxUint16, benchTTL) 339 | // 340 | // benchmarkCacheSet(b, func(key string, value string) { 341 | // cache.Put(key, value) 342 | // }) 343 | //} 344 | // 345 | //// go test -v -bench=^BenchmarkEcache2Set$ -benchtime=1s ./_examples/performance_test.go 346 | //func BenchmarkEcache2Set(b *testing.B) { 347 | // cache := ecache.NewLRUCache(1, math.MaxUint16, benchTTL).LRU2(16) 348 | // 349 | // benchmarkCacheSet(b, func(key string, value string) { 350 | // cache.Put(key, value) 351 | // }) 352 | //} 353 | // 354 | //// go test -v -bench=^BenchmarkBigcacheSet$ -benchtime=1s ./_examples/performance_test.go 355 | //func BenchmarkBigcacheSet(b *testing.B) { 356 | // cache, _ := bigcache.New(context.Background(), bigcache.Config{ 357 | // Shards: 1, 358 | // LifeWindow: benchTTL, 359 | // MaxEntriesInWindow: benchMaxEntries, 360 | // Verbose: false, 361 | // }) 362 | // 363 | // benchmarkCacheSet(b, func(key string, value string) { 364 | // cache.Set(key, []byte(value)) 365 | // }) 366 | //} 367 | // 368 | //// go test -v -bench=^BenchmarkFreecacheSet$ -benchtime=1s ./_examples/performance_test.go 369 | //func BenchmarkFreecacheSet(b *testing.B) { 370 | // cache := freecache.NewCache(benchMaxEntries) 371 | // 372 | // benchmarkCacheSet(b, func(key string, value string) { 373 | // cache.Set([]byte(key), []byte(value), int(benchTTL.Seconds())) 374 | // }) 375 | //} 376 | // 377 | //// go test -v -bench=^BenchmarkGoCacheSet$ -benchtime=1s ./_examples/performance_test.go 378 | //func BenchmarkGoCacheSet(b *testing.B) { 379 | // cache := gocache.New(benchTTL, 0) 380 | // 381 | // benchmarkCacheSet(b, func(key string, value string) { 382 | // cache.Set(key, value, benchTTL) 383 | // }) 384 | //} 385 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2020] [FishGoddess] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------