├── 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 |
--------------------------------------------------------------------------------
/_icons/godoc.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/_icons/license.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 | [](https://pkg.go.dev/github.com/FishGoddess/cachego)
4 | [](https://www.apache.org/licenses/LICENSE-2.0.html)
5 | [](_icons/coverage.svg)
6 | 
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 | [](https://star-history.com/#fishgoddess/cachego&Date)
157 |
--------------------------------------------------------------------------------
/README.en.md:
--------------------------------------------------------------------------------
1 | # 🍰 cachego
2 |
3 | [](https://pkg.go.dev/github.com/FishGoddess/cachego)
4 | [](https://www.apache.org/licenses/LICENSE-2.0.html)
5 | [](_icons/coverage.svg)
6 | 
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 | [](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 |
--------------------------------------------------------------------------------