go-cache
8
总安装量
7
周安装量
#35151
全站排名
安装命令
npx skills add https://github.com/cristiano-pacheco/ai-tools --skill go-cache
Agent 安装分布
gemini-cli
7
claude-code
7
github-copilot
7
codex
7
amp
7
kimi-cli
7
Skill 文档
Go Cache
Generate two files for every cache: a port interface and a Redis-backed implementation.
Which Variant?
Pick before writing anything:
| Scenario | Variant | Get return type |
|---|---|---|
| Flag, existence check, rate limit | Boolean flag | bool |
| Structured data â tokens, sessions, profiles | JSON data | *dto.XxxData |
For TTL:
- Fixed TTL â short-lived or individually written entries (OTPs, OAuth state, rate limits, sessions)
- Randomized TTL â long-lived entries written in bulk (activation flags, daily metrics) â prevents cache stampede
Two-File Pattern
Every cache requires exactly two files:
- Port interface:
internal/modules/<module>/ports/<cache_name>_cache.go - Cache implementation:
internal/modules/<module>/cache/<cache_name>_cache.go
File Layout Order
- Constants (key prefix, TTL)
- Implementation struct (
XxxCache) - Compile-time interface assertion
- Constructor (
NewXxxCache) - Methods (
Set,Get,Delete) - Helper methods (
buildKey,calculateTTL)
Boolean Flag Cache
Use when caching simple existence flags, presence checks, or rate limit states.
- Store
"1"as the value - Return
false, nilwhen the key doesn’t exist (not an error)
Port
package ports
import "context"
// XxxCache describes ...
type XxxCache interface {
Set(ctx context.Context, id uint64) error
Get(ctx context.Context, id uint64) (bool, error)
Delete(ctx context.Context, id uint64) error
}
Implementation
package cache
import (
"context"
"errors"
"fmt"
"time"
"github.com/cristiano-pacheco/bricks/pkg/redis"
"github.com/cristiano-pacheco/pingo/internal/modules/<module>/ports"
redislib "github.com/redis/go-redis/v9"
)
const (
entityCacheKeyPrefix = "entity_name:"
entityCacheTTL = 10 * time.Minute
)
type EntityCache struct {
redisClient redis.UniversalClient
}
var _ ports.EntityCache = (*EntityCache)(nil)
func NewEntityCache(redisClient redis.UniversalClient) *EntityCache {
return &EntityCache{
redisClient: redisClient,
}
}
func (c *EntityCache) Set(ctx context.Context, id uint64) error {
key := c.buildKey(id)
return c.redisClient.Set(ctx, key, "1", entityCacheTTL).Err()
}
func (c *EntityCache) Get(ctx context.Context, id uint64) (bool, error) {
key := c.buildKey(id)
result := c.redisClient.Get(ctx, key)
if err := result.Err(); err != nil {
if errors.Is(err, redislib.Nil) {
return false, nil
}
return false, err
}
return true, nil
}
func (c *EntityCache) Delete(ctx context.Context, id uint64) error {
key := c.buildKey(id)
return c.redisClient.Del(ctx, key).Err()
}
func (c *EntityCache) buildKey(id uint64) string {
return fmt.Sprintf("%s%d", entityCacheKeyPrefix, id)
}
JSON Data Cache
Use when caching structured data. Data structs are defined in the dto package, never in ports.
- Serialize with
json.Marshalbefore storing - Deserialize with
json.Unmarshalwhen retrieving - Return
nil, nilon missing key â unless the key is always expected to exist, in which case return a domain error (e.g.,errs.ErrXxxNotFound) - Use distinct variable names (
getErr,unmarshalErr) to avoid shadowing
Port
package ports
import (
"context"
"github.com/cristiano-pacheco/pingo/internal/modules/<module>/dto"
}
// XxxCache describes ...
type XxxCache interface {
Set(ctx context.Context, key string, data dto.XxxData) error
Get(ctx context.Context, key string) (dto.XxxData, error)
Delete(ctx context.Context, key string) error
}
Implementation
package cache
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/cristiano-pacheco/bricks/pkg/redis"
"github.com/cristiano-pacheco/pingo/internal/modules/<module>/dto"
"github.com/cristiano-pacheco/pingo/internal/modules/<module>/ports"
redislib "github.com/redis/go-redis/v9"
)
const (
entityCacheKeyPrefix = "entity_name:"
entityCacheTTL = 10 * time.Minute
)
type EntityCache struct {
redisClient redis.UniversalClient
}
var _ ports.EntityCache = (*EntityCache)(nil)
func NewEntityCache(redisClient redis.UniversalClient) *EntityCache {
return &EntityCache{
redisClient: redisClient,
}
}
func (c *EntityCache) Set(ctx context.Context, key string, data dto.EntityData) error {
cacheKey := c.buildKey(key)
jsonData, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("marshal entity data: %w", err)
}
return c.redisClient.Set(ctx, cacheKey, jsonData, entityCacheTTL).Err()
}
func (c *EntityCache) Get(ctx context.Context, key string) (dto.EntityData, error) {
cacheKey := c.buildKey(key)
result := c.redisClient.Get(ctx, cacheKey)
if getErr := result.Err(); getErr != nil {
if errors.Is(getErr, redislib.Nil) {
return dto.EntityData{}, nil
}
return dto.EntityData{}, getErr
}
jsonData, err := result.Bytes()
if err != nil {
return dto.EntityData{}, fmt.Errorf("get bytes: %w", err)
}
var entityData dto.EntityData
if unmarshalErr := json.Unmarshal(jsonData, &entityData); unmarshalErr != nil {
return dto.EntityData{}, fmt.Errorf("unmarshal entity data: %w", unmarshalErr)
}
return entityData, nil
}
func (c *EntityCache) Delete(ctx context.Context, key string) error {
cacheKey := c.buildKey(key)
return c.redisClient.Del(ctx, cacheKey).Err()
}
func (c *EntityCache) buildKey(key string) string {
return entityCacheKeyPrefix + key
}
Key Building
String ID (simple concatenation):
func (c *EntityCache) buildKey(id string) string {
return entityCacheKeyPrefix + id
}
Uint64 ID:
func (c *EntityCache) buildKey(id uint64) string {
return fmt.Sprintf("%s%d", entityCacheKeyPrefix, id)
}
Composite key:
func (c *EntityCache) buildKey(userID uint64, resourceID string) string {
return fmt.Sprintf("%s%d:%s", entityCacheKeyPrefix, userID, resourceID)
}
TTL Configuration
Fixed TTL â for short-lived data where stampede is not a concern:
const (
entityCacheKeyPrefix = "entity_name:"
entityCacheTTL = 10 * time.Minute
)
Randomized TTL â for long-lived data created in bulk (prevents cache stampede):
import "math/rand"
const (
entityCacheKeyPrefix = "entity_name:"
entityCacheTTLMin = 23 * time.Hour
entityCacheTTLMax = 25 * time.Hour
)
func (c *EntityCache) calculateTTL() time.Duration {
min := entityCacheTTLMin.Milliseconds()
max := entityCacheTTLMax.Milliseconds()
randomMs := min + rand.Int63n(max-min+1)
return time.Duration(randomMs) * time.Millisecond
}
Common TTL ranges:
5-15 minutesâ OTP codes, OAuth state, rate limits50-70 minutesâ User sessions12-25 hoursâ Activation flags, daily metrics6.5-7.5 daysâ Weekly aggregations
Naming
- Port interface:
XxxCache(portspackage, no suffix) - Implementation struct:
XxxCache(cachepackage â same name, disambiguated by package) - Constructor:
NewXxxCache, returns*XxxCache - Constants: lowercase, package-level (e.g.
entityCacheKeyPrefix,entityCacheTTL)
Fx Wiring
Add to internal/modules/<module>/module.go:
fx.Provide(
fx.Annotate(
cache.NewXxxCache,
fx.As(new(ports.XxxCache)),
),
),
Dependencies
redis.UniversalClientfrom"github.com/cristiano-pacheco/bricks/pkg/redis"redislib "github.com/redis/go-redis/v9"for nil detection
Critical Rules
- Two files: Port in
ports/, implementation incache/ - Interface assertion:
var _ ports.XxxCache = (*XxxCache)(nil)immediately below the struct - Constructor: Returns
*XxxCache(pointer) - Context: Always accept
ctx context.Contextas first parameter â never callcontext.Background()internally - Redis nil: Import
redislib "github.com/redis/go-redis/v9"and check witherrors.Is(err, redislib.Nil) - TTL scope: TTL is an implementation detail â never expose it as a method parameter
- buildKey: Always use a
buildKey()helper;+for string IDs,fmt.Sprintffor numeric IDs - Missing keys: Boolean cache returns
false, nil; JSON cache returnsnil, nil(or a domain error if the key must exist) - DTOs in dto package: Data structs belong in
dto/, never defined inline inports/ - No method comments: Only port interfaces get doc comments; implementation methods do not
- Error messages:
"action noun: %w"format (e.g.,"marshal oauth state: %w","get bytes: %w")
Workflow
- Decide variant: Boolean flag or JSON data?
- Create port interface in
ports/<name>_cache.go - Create cache implementation in
cache/<name>_cache.go - Add Fx wiring to
module.go - Run
make lint - Run
make nilaway