diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 26a99ee..0000000 --- a/.gitattributes +++ /dev/null @@ -1,15 +0,0 @@ -# Set the default behavior, in case people don't have core.autocrlf set. -* text=auto - -# convert to lf on checkout. -*.go text eol=lf -*.mod text eol=lf -*.sum text eol=lf -*.md text eol=lf -*.svg text eol=lf -*.conf text eol=lf - -# Denote all files that are truly binary and should not be modified. -*.png binary -*.jpg binary -*.jpeg binary \ No newline at end of file diff --git a/FUTURE.md b/FUTURE.md index 6bbb58e..1f6a120 100644 --- a/FUTURE.md +++ b/FUTURE.md @@ -1,9 +1,12 @@ ## ✒ 未来版本的新特性 (Features in future versions) +### v1.0.0 + +* [x] 稳定的 API + ### v0.6.x -* [ ] 梳理代码,优化代码风格,精简部分代码和注释 -* [ ] 完善监控上报器,提供更多缓存信息查询的方法 +* [x] 梳理代码,优化代码风格,精简部分代码和注释 ### v0.5.x diff --git a/HISTORY.md b/HISTORY.md index 6a701f0..176c1d9 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,12 @@ ## ✒ 历史版本的特性介绍 (Features in old versions) +### v1.0.0 + +> 此版本发布于 2025-03-15 + +* 稳定的 API 版本 +* 调整快速时钟的代码,更改包名为 fastclock + ### v0.6.1 > 此版本发布于 2024-01-18 diff --git a/README.en.md b/README.en.md index a835b6b..11faeed 100644 --- a/README.en.md +++ b/README.en.md @@ -23,7 +23,8 @@ * Sentinel cleanup supports, cleaning up at fixed duration * Singleflight supports, which can decrease the times of cache penetration * Timer task supports, which is convenient to load data to cache -* Report supports, providing several reporting points. +* Report supports, providing several reporting points +* Fast clock supports, fetching current time in nanoseconds _Check [HISTORY.md](./HISTORY.md) and [FUTURE.md](./FUTURE.md) to get more information._ @@ -143,11 +144,7 @@ BenchmarkGoCacheSet-12 4921483 249.0 ns/op > Notice: Ecache only has lru mode, including v1 and v2; Freecache has 256 shardings, and we can't reset to 1. -> Benchmarks: [_examples/performance_test.go](./_examples/performance_test.go) - -As you can see, cachego has a higher performance with sharding, but sharding has one-more-time positioning -operation, so if the locking cost is less than the cost of positioning, this sharding is dragging. However, it has -better performance in most time. +> Benchmarks: [_examples/performance_test.go](./_examples/performance_test.go). ### 👥 Contributors diff --git a/README.md b/README.md index 8a07100..62585ed 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ * 自带 singleflight 机制,减少缓存穿透的伤害 * 自带定时任务封装,方便热数据定时加载到缓存 * 支持上报缓存状况,可自定义多个缓存上报点 +* 自带快速时钟,支持纳秒级获取时间 _历史版本的特性请查看 [HISTORY.md](./HISTORY.md)。未来版本的新特性和计划请查看 [FUTURE.md](./FUTURE.md)。_ @@ -142,10 +143,7 @@ BenchmarkGoCacheSet-12 4921483 249.0 ns/op > 注:Ecache 只有 LRU 模式,v1 和 v2 两个版本;Freecache 默认是 256 分片,无法调节为 1 个分片进行对比测试。 -> 测试文件:[_examples/performance_test.go](./_examples/performance_test.go) - -可以看出,使用分片机制后的读写性能非常高,但是分片会多一次哈希定位的操作,如果加锁的消耗小于定位的消耗,那分片就不占优势。 -不过在绝大多数的情况下,分片机制带来的性能提升都是巨大的,尤其是对写操作较多的 lru 和 lfu 实现。 +> 测试文件:[_examples/performance_test.go](./_examples/performance_test.go)。 ### 👥 贡献者 diff --git a/_examples/basic.go b/_examples/basic.go index 47b9f67..ec3f756 100644 --- a/_examples/basic.go +++ b/_examples/basic.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/_examples/clock.go b/_examples/fast_clock.go similarity index 78% rename from _examples/clock.go rename to _examples/fast_clock.go index fb228c6..5bb219b 100644 --- a/_examples/clock.go +++ b/_examples/fast_clock.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,21 +20,17 @@ import ( "time" "github.com/FishGoddess/cachego" - "github.com/FishGoddess/cachego/pkg/clock" + "github.com/FishGoddess/cachego/pkg/fastclock" ) func main() { - // Create a fast clock and get current time in nanosecond by Now. - c := clock.New() - c.Now() - // Fast clock may return an "incorrect" time compared with time.Now. // The gap will be smaller than about 100 ms. for i := 0; i < 10; i++ { time.Sleep(time.Duration(rand.Int63n(int64(time.Second)))) timeNow := time.Now().UnixNano() - clockNow := c.Now() + clockNow := fastclock.NowNanos() fmt.Println(timeNow) fmt.Println(clockNow) @@ -43,8 +39,8 @@ func main() { } // You can specify the fast clock to cache by WithNow. - // All getting current time operations in this cache will use fast clock. - cache := cachego.NewCache(cachego.WithNow(clock.New().Now)) + // All time used in this cache will be got from fast clock. + cache := cachego.NewCache(cachego.WithNow(fastclock.NowNanos)) cache.Set("key", 666, 100*time.Millisecond) value, ok := cache.Get("key") diff --git a/_examples/gc.go b/_examples/gc.go index d30816f..80e51eb 100644 --- a/_examples/gc.go +++ b/_examples/gc.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/_examples/lfu.go b/_examples/lfu.go index 964f4e9..547d737 100644 --- a/_examples/lfu.go +++ b/_examples/lfu.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/_examples/load.go b/_examples/load.go index da22868..66ab77b 100644 --- a/_examples/load.go +++ b/_examples/load.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/_examples/lru.go b/_examples/lru.go index d983bef..31cfe44 100644 --- a/_examples/lru.go +++ b/_examples/lru.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/_examples/performance_test.go b/_examples/performance_test.go index 9a9be38..5d29f29 100644 --- a/_examples/performance_test.go +++ b/_examples/performance_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/_examples/report.go b/_examples/report.go index c7c69bc..3d29a85 100644 --- a/_examples/report.go +++ b/_examples/report.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/_examples/sharding.go b/_examples/sharding.go index b310200..fb6ce91 100644 --- a/_examples/sharding.go +++ b/_examples/sharding.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/_examples/task.go b/_examples/task.go index db814ad..b920917 100644 --- a/_examples/task.go +++ b/_examples/task.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/_examples/ttl.go b/_examples/ttl.go index 936ec4f..59dc4a0 100644 --- a/_examples/ttl.go +++ b/_examples/ttl.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cache.go b/cache.go index e838fa4..383e3f0 100644 --- a/cache.go +++ b/cache.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cache_test.go b/cache_test.go index 72413e7..33c903c 100644 --- a/cache_test.go +++ b/cache_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cache_type.go b/cache_type.go index 95a65ea..26a3dbc 100644 --- a/cache_type.go +++ b/cache_type.go @@ -1,4 +1,4 @@ -// Copyright 2024 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cache_type_test.go b/cache_type_test.go index fba7442..17201d1 100644 --- a/cache_type_test.go +++ b/cache_type_test.go @@ -1,4 +1,4 @@ -// Copyright 2024 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/config.go b/config.go index 2069c69..85d612d 100644 --- a/config.go +++ b/config.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/config_test.go b/config_test.go index 6491aa5..fa05dbb 100644 --- a/config_test.go +++ b/config_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/doc.go b/doc.go deleted file mode 100644 index 40a5e7d..0000000 --- a/doc.go +++ /dev/null @@ -1,478 +0,0 @@ -// Copyright 2020 FishGoddess. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/* -Package cachego provides an easy way to use foundation for your caching operations. - -1. basic: - - // Use NewCache function to create a cache. - // By default, it creates a standard cache which evicts entries randomly. - // Use WithShardings to shard cache to several parts for higher performance. - // Use WithGC to clean expired entries every 10 minutes. - cache := cachego.NewCache(cachego.WithGC(10*time.Minute), cachego.WithShardings(64)) - - // Set an entry to cache with ttl. - cache.Set("key", 123, time.Second) - - // Get an entry from cache. - value, ok := cache.Get("key") - fmt.Println(value, ok) // 123 true - - // Check how many entries stores in cache. - size := cache.Size() - fmt.Println(size) // 1 - - // Clean expired entries. - cleans := cache.GC() - fmt.Println(cleans) // 1 - - // Set an entry which doesn't have ttl. - cache.Set("key", 123, cachego.NoTTL) - - // Remove an entry. - removedValue := cache.Remove("key") - fmt.Println(removedValue) // 123 - - // Reset resets cache to initial status. - cache.Reset() - - // Get value from cache and load it to cache if not found. - value, ok = cache.Get("key") - if !ok { - // Loaded entry will be set to cache and returned. - // By default, it will use singleflight. - value, _ = cache.Load("key", time.Second, func() (value interface{}, err error) { - return 666, nil - }) - } - - fmt.Println(value) // 666 - - // You can use WithLRU to specify the type of cache to lru. - // Also, try WithLFU if you want to use lfu to evict data. - cache = cachego.NewCache(cachego.WithLRU(100)) - cache = cachego.NewCache(cachego.WithLFU(100)) - - // Use NewCacheWithReport to create a cache with report. - cache, reporter := cachego.NewCacheWithReport(cachego.WithCacheName("test")) - fmt.Println(reporter.CacheName()) - fmt.Println(reporter.CacheType()) - -2. ttl: - - cache := cachego.NewCache() - - // We think most of the entries in cache should have its ttl. - // So set an entry to cache should specify a ttl. - cache.Set("key", 666, time.Second) - - value, ok := cache.Get("key") - fmt.Println(value, ok) // 666 true - - time.Sleep(2 * time.Second) - - // The entry is expired after ttl. - value, ok = cache.Get("key") - fmt.Println(value, ok) // false - - // Notice that the entry still stores in cache even if it's expired. - // This is because we think you will reset entry to cache after cache missing in most situations. - // So we can reuse this entry and just reset its value and ttl. - size := cache.Size() - fmt.Println(size) // 1 - - // What should I do if I want an expired entry never storing in cache? Try GC: - cleans := cache.GC() - fmt.Println(cleans) // 1 - - size = cache.Size() - fmt.Println(size) // 0 - - // However, not all entries have ttl, and you can specify a NoTTL constant to do so. - // In fact, the entry won't expire as long as its ttl is <= 0. - // So you may have known NoTTL is a "readable" value of "<= 0". - cache.Set("key", 666, cachego.NoTTL) - -3. lru: - - // By default, NewCache() returns a standard cache which evicts entries randomly. - cache := cachego.NewCache(cachego.WithMaxEntries(10)) - - for i := 0; i < 20; i++ { - key := strconv.Itoa(i) - cache.Set(key, i, cachego.NoTTL) - } - - // Since we set 20 entries to cache, the size won't be 20 because we limit the max entries to 10. - size := cache.Size() - fmt.Println(size) // 10 - - // We don't know which entries will be evicted and stayed. - for i := 0; i < 20; i++ { - key := strconv.Itoa(i) - value, ok := cache.Get(key) - fmt.Println(key, value, ok) - } - - fmt.Println() - - // Sometimes we want it evicts entries by lru, try WithLRU. - // You need to specify the max entries storing in lru cache. - // More details see https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU). - cache = cachego.NewCache(cachego.WithLRU(10)) - - for i := 0; i < 20; i++ { - key := strconv.Itoa(i) - cache.Set(key, i, cachego.NoTTL) - } - - // Only the least recently used entries can be got in a lru cache. - for i := 0; i < 20; i++ { - key := strconv.Itoa(i) - value, ok := cache.Get(key) - fmt.Println(key, value, ok) - } - - // By default, lru will share one lock to do all operations. - // You can sharding cache to several parts for higher performance. - // Notice that max entries only effect to one part in sharding mode. - // For example, the total max entries will be 2*10 if shardings is 2 and max entries is 10 in WithLRU or WithMaxEntries. - // In some cache libraries, they will calculate max entries in each parts of shardings, like 10/2. - // However, the result divided by max entries and shardings may be not an integer which will make the total max entries incorrect. - // So we let users decide the exact max entries in each parts of shardings. - cache = cachego.NewCache(cachego.WithShardings(2), cachego.WithLRU(10)) - -4. lfu: - - // By default, NewCache() returns a standard cache which evicts entries randomly. - cache := cachego.NewCache(cachego.WithMaxEntries(10)) - - for i := 0; i < 20; i++ { - key := strconv.Itoa(i) - cache.Set(key, i, cachego.NoTTL) - } - - // Since we set 20 entries to cache, the size won't be 20 because we limit the max entries to 10. - size := cache.Size() - fmt.Println(size) // 10 - - // We don't know which entries will be evicted and stayed. - for i := 0; i < 20; i++ { - key := strconv.Itoa(i) - value, ok := cache.Get(key) - fmt.Println(key, value, ok) - } - - fmt.Println() - - // Sometimes we want it evicts entries by lfu, try WithLFU. - // You need to specify the max entries storing in lfu cache. - // More details see https://en.wikipedia.org/wiki/Cache_replacement_policies#Least-frequently_used_(LFU). - cache = cachego.NewCache(cachego.WithLFU(10)) - - for i := 0; i < 20; i++ { - key := strconv.Itoa(i) - - // Let entries have some frequently used operations. - for j := 0; j < i; j++ { - cache.Set(key, i, cachego.NoTTL) - } - } - - for i := 0; i < 20; i++ { - key := strconv.Itoa(i) - value, ok := cache.Get(key) - fmt.Println(key, value, ok) - } - - // By default, lfu will share one lock to do all operations. - // You can sharding cache to several parts for higher performance. - // Notice that max entries only effect to one part in sharding mode. - // For example, the total max entries will be 2*10 if shardings is 2 and max entries is 10 in WithLFU or WithMaxEntries. - // In some cache libraries, they will calculate max entries in each parts of shardings, like 10/2. - // However, the result divided by max entries and shardings may be not an integer which will make the total max entries incorrect. - // So we let users decide the exact max entries in each parts of shardings. - cache = cachego.NewCache(cachego.WithShardings(2), cachego.WithLFU(10)) - -5. sharding: - - // All operations in cache share one lock for concurrency. - // Use read lock or write lock is depends on cache implements. - // Get will use read lock in standard cache, but lru and lfu don't. - // This may be a serious performance problem in high qps. - cache := cachego.NewCache() - - // We provide a sharding cache wrapper to shard one cache to several parts with hash. - // Every parts store its entries and all operations of one entry work on one part. - // This means there are more than one lock when you operate entries. - // The performance will be better in high qps. - cache = cachego.NewCache(cachego.WithShardings(64)) - cache.Set("key", 666, cachego.NoTTL) - - value, ok := cache.Get("key") - fmt.Println(value, ok) // 666 true - - // Notice that max entries will be the sum of shards. - // For example, we set WithShardings(4) and WithMaxEntries(100), and the max entries in whole cache will be 4 * 100. - cache = cachego.NewCache(cachego.WithShardings(4), cachego.WithMaxEntries(100)) - - for i := 0; i < 1000; i++ { - key := strconv.Itoa(i) - cache.Set(key, i, cachego.NoTTL) - } - - size := cache.Size() - fmt.Println(size) // 400 - -6. gc: - - cache := cachego.NewCache() - cache.Set("key", 666, time.Second) - - time.Sleep(2 * time.Second) - - // The entry is expired after ttl. - value, ok := cache.Get("key") - fmt.Println(value, ok) // false - - // As you know the entry still stores in cache even if it's expired. - // This is because we think you will reset entry to cache after cache missing in most situations. - // So we can reuse this entry and just reset its value and ttl. - size := cache.Size() - fmt.Println(size) // 1 - - // What should I do if I want an expired entry never storing in cache? Try GC: - cleans := cache.GC() - fmt.Println(cleans) // 1 - - // Is there a smart way to do that? Try WithGC: - // For testing, we set a small duration of gc. - // You should set at least 3 minutes in production for performance. - cache = cachego.NewCache(cachego.WithGC(2 * time.Second)) - cache.Set("key", 666, time.Second) - - size = cache.Size() - fmt.Println(size) // 1 - - time.Sleep(3 * time.Second) - - size = cache.Size() - fmt.Println(size) // 0 - - // Or you want a cancalable gc task? Try RunGCTask: - cache = cachego.NewCache() - cancel := cachego.RunGCTask(cache, 2*time.Second) - - cache.Set("key", 666, time.Second) - - size = cache.Size() - fmt.Println(size) // 1 - - time.Sleep(3 * time.Second) - - size = cache.Size() - fmt.Println(size) // 0 - - cancel() - - cache.Set("key", 666, time.Second) - - size = cache.Size() - fmt.Println(size) // 1 - - time.Sleep(3 * time.Second) - - size = cache.Size() - fmt.Println(size) // 1 - - // By default, gc only scans at most maxScans entries one time to remove expired entries. - // This is because scans all entries may cost much time if there is so many entries in cache, and a "stw" will happen. - // This can be a serious problem in some situations. - // Use WithMaxScans to set this value, remember, a value <= 0 means no scan limit. - cache = cachego.NewCache(cachego.WithGC(10*time.Minute), cachego.WithMaxScans(0)) - -7. load: - - // By default, singleflight is enabled in cache. - // Use WithDisableSingleflight to disable if you want. - cache := cachego.NewCache(cachego.WithDisableSingleflight()) - - // We recommend you to use singleflight. - cache = cachego.NewCache() - - value, ok := cache.Get("key") - fmt.Println(value, ok) // false - - if !ok { - // Load loads a value of key to cache with ttl. - // Use cachego.NoTTL if you want this value is no ttl. - // After loading value to cache, it returns the loaded value and error if failed. - value, _ = cache.Load("key", time.Second, func() (value interface{}, err error) { - return 666, nil - }) - } - - fmt.Println(value) // 666 - - value, ok = cache.Get("key") - fmt.Println(value, ok) // 666, true - - time.Sleep(2 * time.Second) - - value, ok = cache.Get("key") - fmt.Println(value, ok) // , false - -8. report: - - func reportMissed(reporter *cachego.Reporter, key string) { - fmt.Printf("report: missed key %s, missed rate %.3f\n", key, reporter.MissedRate()) - } - - func reportHit(reporter *cachego.Reporter, key string, value interface{}) { - fmt.Printf("report: hit key %s value %+v, hit rate %.3f\n", key, value, reporter.HitRate()) - } - - func reportGC(reporter *cachego.Reporter, cost time.Duration, cleans int) { - fmt.Printf("report: gc cost %s cleans %d, gc count %d, cache size %d\n", cost, cleans, reporter.CountGC(), reporter.CacheSize()) - } - - func reportLoad(reporter *cachego.Reporter, key string, value interface{}, ttl time.Duration, err error) { - fmt.Printf("report: load key %s value %+v ttl %s, err %+v, load count %d\n", key, value, ttl, err, reporter.CountLoad()) - } - - // We provide some ways to report the status of cache. - // Use NewCacheWithReport to create a cache with reporting features. - cache, reporter := cachego.NewCacheWithReport( - // Sometimes you may have several caches in one service. - // You can set each name by WithCacheName and get the name from reporter. - cachego.WithCacheName("test"), - - // For testing... - cachego.WithMaxEntries(3), - cachego.WithGC(100*time.Millisecond), - - // ReportMissed reports the missed key getting from cache. - // ReportHit reports the hit entry getting from cache. - // ReportGC reports the status of cache gc. - // ReportLoad reports the result of loading. - cachego.WithReportMissed(reportMissed), - cachego.WithReportHit(reportHit), - cachego.WithReportGC(reportGC), - cachego.WithReportLoad(reportLoad), - ) - - for i := 0; i < 5; i++ { - key := strconv.Itoa(i) - evictedValue := cache.Set(key, key, 10*time.Millisecond) - fmt.Println(evictedValue) - } - - for i := 0; i < 5; i++ { - key := strconv.Itoa(i) - value, ok := cache.Get(key) - fmt.Println(value, ok) - } - - time.Sleep(200 * time.Millisecond) - - value, err := cache.Load("key", time.Second, func() (value interface{}, err error) { - return 666, io.EOF - }) - - fmt.Println(value, err) - - // These are some useful methods of reporter. - fmt.Println("CacheName:", reporter.CacheName()) - fmt.Println("CacheType:", reporter.CacheType()) - fmt.Println("CountMissed:", reporter.CountMissed()) - fmt.Println("CountHit:", reporter.CountHit()) - fmt.Println("CountGC:", reporter.CountGC()) - fmt.Println("CountLoad:", reporter.CountLoad()) - fmt.Println("CacheSize:", reporter.CacheSize()) - fmt.Println("MissedRate:", reporter.MissedRate()) - fmt.Println("HitRate:", reporter.HitRate()) - -9. task: - - var ( - contextKey = struct{}{} - ) - - func beforePrint(ctx context.Context) { - fmt.Println("before:", ctx.Value(contextKey)) - } - - func afterPrint(ctx context.Context) { - fmt.Println("after:", ctx.Value(contextKey)) - } - - func printContextValue(ctx context.Context) { - fmt.Println("context value:", ctx.Value(contextKey)) - } - - // Create a context to stop the task. - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - // Wrap context with key and value - ctx = context.WithValue(ctx, contextKey, "hello") - - // Use New to create a task and run it. - // You can use it to load some hot data to cache at fixed duration. - // Before is called before the task loop, optional. - // After is called after the task loop, optional. - // Context is passed to fn include fn/before/after which can stop the task by Done(), optional. - // Duration is the duration between two loop of fn, optional. - // Run will start a new goroutine and run the task loop. - // The task will stop if context is done. - task.New(printContextValue).Before(beforePrint).After(afterPrint).Context(ctx).Duration(time.Second).Run() - -10. clock: - - // Create a fast clock and get current time in nanosecond by Now. - c := clock.New() - c.Now() - - // Fast clock may return an "incorrect" time compared with time.Now. - // The gap will be smaller than about 100 ms. - for i := 0; i < 10; i++ { - time.Sleep(time.Duration(rand.Int63n(int64(time.Second)))) - - timeNow := time.Now().UnixNano() - clockNow := c.Now() - - fmt.Println(timeNow) - fmt.Println(clockNow) - fmt.Println("gap:", time.Duration(timeNow-clockNow)) - fmt.Println() - } - - // You can specify the fast clock to cache by WithNow. - // All getting current time operations in this cache will use fast clock. - cache := cachego.NewCache(cachego.WithNow(clock.New().Now)) - cache.Set("key", 666, 100*time.Millisecond) - - value, ok := cache.Get("key") - fmt.Println(value, ok) // 666, true - - time.Sleep(200 * time.Millisecond) - - value, ok = cache.Get("key") - fmt.Println(value, ok) // , false -*/ -package cachego // import "github.com/FishGoddess/cachego" - -// Version is the version string representation of cachego. -const Version = "v0.6.1" diff --git a/entry.go b/entry.go index 6316f61..884980c 100644 --- a/entry.go +++ b/entry.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,9 +17,11 @@ package cachego import "time" type entry struct { - key string - value interface{} - expiration int64 // Time in nanosecond, valid util 2262 year (enough, uh?) + key string + value interface{} + + // Time in nanosecond, valid util 2262 year (enough, right?) + expiration int64 now func() int64 } diff --git a/entry_test.go b/entry_test.go index ed3e08d..32f0177 100644 --- a/entry_test.go +++ b/entry_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/global.go b/global.go index 890d914..0cd391c 100644 --- a/global.go +++ b/global.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/global_test.go b/global_test.go index 9a113b3..3e38107 100644 --- a/global_test.go +++ b/global_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/go.mod b/go.mod index a1f8805..f620f40 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/FishGoddess/cachego -go 1.20 +go 1.21 diff --git a/lfu.go b/lfu.go index 1705f4b..f6816dd 100644 --- a/lfu.go +++ b/lfu.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/lfu_test.go b/lfu_test.go index edaaf51..c9333d8 100644 --- a/lfu_test.go +++ b/lfu_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/load.go b/load.go index b5e3f76..20cd4f0 100644 --- a/load.go +++ b/load.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/load_test.go b/load_test.go index 0b473f9..abbf86b 100644 --- a/load_test.go +++ b/load_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/lru.go b/lru.go index f023c77..34c1ee7 100644 --- a/lru.go +++ b/lru.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/lru_test.go b/lru_test.go index 0060ddf..755fbbf 100644 --- a/lru_test.go +++ b/lru_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/option.go b/option.go index 192dda2..2e90766 100644 --- a/option.go +++ b/option.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/option_test.go b/option_test.go index 5a64b8e..0d5681a 100644 --- a/option_test.go +++ b/option_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/pkg/clock/clock.go b/pkg/clock/clock.go deleted file mode 100644 index 6a3345e..0000000 --- a/pkg/clock/clock.go +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package clock - -import ( - "sync" - "sync/atomic" - "time" -) - -const ( - duration = 100 * time.Millisecond -) - -var ( - clock *Clock - clockOnce sync.Once -) - -// Clock is a fast clock for getting current time. -// It caches time and updates it in fixed duration which may return an "incorrect" time compared with time.Now().UnixNano(). -// In fact, we don't recommend you to use it unless you have to... -// According to our benchmarks, it does run faster than time.Now: -// -// In my win10 pc: -// goos: windows -// goarch: amd64 -// cpu: AMD Ryzen 7 5800X 8-Core Processor -// BenchmarkTimeNow-16 338466458 3.523 ns/op 0 B/op 0 allocs/op -// BenchmarkClockNow-16 1000000000 0.2165 ns/op 0 B/op 0 allocs/op -// -// In my macbook with charging: -// goos: darwin -// goarch: amd64 -// pkg: github.com/FishGoddess/cachego/pkg/clock -// cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz -// BenchmarkTimeNow-12 17841402 67.05 ns/op 0 B/op 0 allocs/op -// BenchmarkClockNow-12 1000000000 0.2528 ns/op 0 B/op 0 allocs/op -// -// In my cloud server using 2 cores and running benchmarks in docker container: -// goos: linux -// goarch: amd64 -// pkg: github.com/FishGoddess/cachego/pkg/clock -// cpu: AMD EPYC 7K62 48-Core Processor -// BenchmarkTimeNow-2 17946441 65.62 ns/op 0 B/op 0 allocs/op -// BenchmarkClockNow-2 1000000000 0.3162 ns/op 0 B/op 0 allocs/op -// -// PS: All benchmarks are ran with "go test -bench=. -benchtime=1s". -// -// However, the performance of time.Now is faster enough in many os and is enough for 99.9% situations. -// The another reason choosing to use it is . -// So, better performance should not be the first reason to use it. -// The first reason to use it is reducing gc objects, but we hope you never use it :) -type Clock struct { - now int64 -} - -// New creates a new clock which caches time and updates it in fixed duration. -func New() *Clock { - clockOnce.Do(func() { - clock = &Clock{ - now: time.Now().UnixNano(), - } - - go clock.start() - }) - - return clock -} - -func (c *Clock) start() { - for { - for i := 0; i < 9; i++ { - time.Sleep(duration) - atomic.AddInt64(&c.now, int64(duration)) - } - - time.Sleep(duration) - atomic.StoreInt64(&c.now, time.Now().UnixNano()) - } -} - -// Now returns the current time in nanoseconds. -func (c *Clock) Now() int64 { - return atomic.LoadInt64(&c.now) -} diff --git a/pkg/clock/clock_test.go b/pkg/clock/clock_test.go deleted file mode 100644 index aa70164..0000000 --- a/pkg/clock/clock_test.go +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package clock - -import ( - "math" - "math/rand" - "testing" - "time" -) - -// go test -bench=^BenchmarkTimeNow$ -benchtime=1s ./clock.go ./clock_test.go -func BenchmarkTimeNow(b *testing.B) { - b.ReportAllocs() - b.ResetTimer() - - for i := 0; i < b.N; i++ { - time.Now().Nanosecond() - } -} - -// go test -bench=^BenchmarkClockNow$ -benchtime=1s ./clock.go ./clock_test.go -func BenchmarkClockNow(b *testing.B) { - clock := New() - - b.ReportAllocs() - b.ResetTimer() - - for i := 0; i < b.N; i++ { - clock.Now() - } -} - -// go test -v -cover -count=1 -test.cpu=1 -run=^TestNew$ -func TestNew(t *testing.T) { - var clocks []*Clock - - for i := 0; i < 100; i++ { - clocks = append(clocks, New()) - } - - for i := 0; i < 100; i++ { - if clocks[i] != clock { - t.Fatalf("clocks[i] %p != clock %p", clocks[i], clock) - } - } -} - -// go test -v -cover -count=1 -test.cpu=1 -run=^TestClock$ -func TestClock(t *testing.T) { - testClock := New() - - for i := 0; i < 100; i++ { - now := testClock.Now() - t.Log(now) - - expect := time.Now().UnixNano() - if math.Abs(float64(expect-now)) > float64(duration)*1.5 { - t.Fatalf("now %d is wrong with expect %d", now, expect) - } - - time.Sleep(time.Duration(rand.Int63n(int64(duration)))) - } -} diff --git a/pkg/fastclock/fast_clock.go b/pkg/fastclock/fast_clock.go new file mode 100644 index 0000000..69ba68a --- /dev/null +++ b/pkg/fastclock/fast_clock.go @@ -0,0 +1,83 @@ +// Copyright 2025 FishGoddess. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fastclock + +import ( + "sync" + "sync/atomic" + "time" +) + +// fastClock is a clock for getting current time faster. +// It caches current time in nanos and updates it in fixed duration, so it's not a precise way to get current time. +// 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". +// According to our benchmarks, it does run faster than time.Now: +// +// In my linux server with 2 cores: +// BenchmarkTimeNow-2 19150246 62.26 ns/op 0 B/op 0 allocs/op +// BenchmarkFastClockNow-2 357209233 3.46 ns/op 0 B/op 0 allocs/op +// BenchmarkFastClockNowNanos-2 467461363 2.55 ns/op 0 B/op 0 allocs/op +// +// However, the performance of time.Now is faster enough for 99.9% situations, so we hope you never use it :) +type fastClock struct { + nanos int64 +} + +func newClock() *fastClock { + clock := &fastClock{ + nanos: time.Now().UnixNano(), + } + + go clock.start() + return clock +} + +func (fc *fastClock) start() { + const duration = 100 * time.Millisecond + + for { + for i := 0; i < 9; i++ { + time.Sleep(duration) + atomic.AddInt64(&fc.nanos, int64(duration)) + } + + time.Sleep(duration) + atomic.StoreInt64(&fc.nanos, time.Now().UnixNano()) + } +} + +func (fc *fastClock) currentNanos() int64 { + return atomic.LoadInt64(&fc.nanos) +} + +var ( + clock *fastClock + clockOnce sync.Once +) + +// Now returns the current time from fast clock. +func Now() time.Time { + nanos := NowNanos() + return time.Unix(0, nanos) +} + +// NowNanos returns the current time in nanos from fast clock. +func NowNanos() int64 { + clockOnce.Do(func() { + clock = newClock() + }) + + return clock.currentNanos() +} diff --git a/pkg/fastclock/fast_clock_test.go b/pkg/fastclock/fast_clock_test.go new file mode 100644 index 0000000..27a09ab --- /dev/null +++ b/pkg/fastclock/fast_clock_test.go @@ -0,0 +1,87 @@ +// Copyright 2025 FishGoddess. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fastclock + +import ( + "math" + "math/rand" + "testing" + "time" +) + +// go test -v -run=^$ -bench=^BenchmarkTimeNow$ -benchtime=1s +func BenchmarkTimeNow(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + time.Now() + } +} + +// go test -v -run=^$ -bench=^BenchmarkFastClockNow$ -benchtime=1s +func BenchmarkFastClockNow(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + Now() + } +} + +// go test -v -run=^$ -bench=^BenchmarkFastClockNowNanos$ -benchtime=1s +func BenchmarkFastClockNowNanos(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + NowNanos() + } +} + +// go test -v -cover -count=1 -test.cpu=1 -run=^TestNow$ +func TestNow(t *testing.T) { + duration := 100 * time.Millisecond + + for i := 0; i < 100; i++ { + got := Now() + gap := time.Since(got) + t.Logf("got: %v, gap: %v", got, gap) + + if math.Abs(float64(gap.Nanoseconds())) > float64(duration)*1.1 { + t.Errorf("now %v is wrong", got) + } + + time.Sleep(time.Duration(rand.Int63n(int64(duration)))) + } +} + +// go test -v -cover -count=1 -test.cpu=1 -run=^TestNowNanos$ +func TestNowNanos(t *testing.T) { + duration := 100 * time.Millisecond + + for i := 0; i < 100; i++ { + gotNanos := NowNanos() + got := time.Unix(0, gotNanos) + gap := time.Since(got) + t.Logf("got: %v, gap: %v", got, gap) + + if math.Abs(float64(gap.Nanoseconds())) > float64(duration)*1.1 { + t.Errorf("now %v is wrong", got) + } + + time.Sleep(time.Duration(rand.Int63n(int64(duration)))) + } +} diff --git a/pkg/heap/heap.go b/pkg/heap/heap.go index 8bf74cc..4d8730d 100644 --- a/pkg/heap/heap.go +++ b/pkg/heap/heap.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/pkg/heap/heap_test.go b/pkg/heap/heap_test.go index b74556b..e0362bb 100644 --- a/pkg/heap/heap_test.go +++ b/pkg/heap/heap_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/pkg/singleflight/singleflight.go b/pkg/singleflight/singleflight.go index 306834e..2b857dc 100644 --- a/pkg/singleflight/singleflight.go +++ b/pkg/singleflight/singleflight.go @@ -1,4 +1,4 @@ -// Copyright 2021 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ import ( "sync" ) -// call wraps function with some information and stores result and error after calling. type call struct { fn func() (result interface{}, err error) result interface{} @@ -37,11 +36,10 @@ func newCall(fn func() (result interface{}, err error)) *call { } } -// do will call fn and fill result and error to call. -// Notice: Any panics or runtime.Goexit() happening in fn() will be ignored. func (c *call) do() { defer c.wg.Done() + // Notice: Any panics or runtime.Goexit() happening in fn() will be ignored. c.result, c.err = c.fn() } @@ -65,7 +63,6 @@ func (g *Group) Call(key string, fn func() (interface{}, error)) (interface{}, e if c, ok := g.calls[key]; ok { g.lock.Unlock() - // Waiting... c.wg.Wait() return c.result, c.err } diff --git a/pkg/singleflight/singleflight_test.go b/pkg/singleflight/singleflight_test.go index 7daa777..928826d 100644 --- a/pkg/singleflight/singleflight_test.go +++ b/pkg/singleflight/singleflight_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/pkg/task/task.go b/pkg/task/task.go index b0c11cd..57d955e 100644 --- a/pkg/task/task.go +++ b/pkg/task/task.go @@ -1,4 +1,4 @@ -// Copyright 2022 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/pkg/task/task_test.go b/pkg/task/task_test.go index 3b5fc3d..c0137b4 100644 --- a/pkg/task/task_test.go +++ b/pkg/task/task_test.go @@ -1,4 +1,4 @@ -// Copyright 2022 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/report.go b/report.go index 31915c3..7faa842 100644 --- a/report.go +++ b/report.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/report_test.go b/report_test.go index 9619ed0..c434a72 100644 --- a/report_test.go +++ b/report_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/sharding.go b/sharding.go index 096e5c3..2b012fd 100644 --- a/sharding.go +++ b/sharding.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/sharding_test.go b/sharding_test.go index 71c7d22..e70dee1 100644 --- a/sharding_test.go +++ b/sharding_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/standard.go b/standard.go index 83dd0ad..722e8ba 100644 --- a/standard.go +++ b/standard.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/standard_test.go b/standard_test.go index 3af3b85..cf42b0c 100644 --- a/standard_test.go +++ b/standard_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 FishGoddess. All Rights Reserved. +// Copyright 2025 FishGoddess. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License.