go-create-cache
npx skills add https://github.com/cristiano-pacheco/ai-rules --skill go-create-cache
Agent 安装分布
Skill 文档
Go Create Cache
Generate cache files for Go backend using Redis.
Two-File Pattern
Every cache requires two files:
- Port interface:
internal/modules/<module>/ports/<cache_name>_cache.go - Cache implementation:
internal/modules/<module>/cache/<cache_name>_cache.go
Port File Layout Order
- Interface definition (
XxxCacheâ no suffix)
Cache File Layout Order
- Constants (cache key prefix, TTL)
- Implementation struct (
XxxCache) - Compile-time interface assertion
- Constructor (
NewXxxCache) - Methods (
Set,Get,Delete, etc.) - Helper methods (
buildKey, etc.)
Port Interface Structure
Location: internal/modules/<module>/ports/<cache_name>_cache.go
package ports
type UserActivatedCache interface {
Set(userID uint64) error
Get(userID uint64) (bool, error)
Delete(userID uint64) error
}
Cache Implementation Structure
Location: internal/modules/<module>/cache/<cache_name>_cache.go
package cache
import (
"context"
"errors"
"fmt"
"time"
"github.com/cristiano-pacheco/bricks/pkg/redis"
"github.com/cristiano-pacheco/pingo/internal/modules/<module>/ports"
)
const (
cacheKeyPrefix = "entity_name:"
cacheTTLMin = 23 * time.Hour
cacheTTLMax = 25 * time.Hour
)
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(id uint64) error {
key := c.buildKey(id)
ctx := context.Background()
ttl := c.calculateTTL()
return c.redisClient.Set(ctx, key, "1", ttl).Err()
}
func (c *EntityCache) calculateTTL() time.Duration {
min := cacheTTLMin.Milliseconds()
max := cacheTTLMax.Milliseconds()
randomMs := min + rand.Int63n(max-min+1)
return time.Duration(randomMs) * time.Millisecond
}
func (c *EntityCache) Get(id uint64) (bool, error) {
key := c.buildKey(id)
ctx := context.Background()
result := c.redisClient.Get(ctx, key)
if err := result.Err(); err != nil {
if errors.Is(err, redisClient.Nil) {
return false, nil // Key does not exist
}
return false, err
}
return true, nil
}
func (c *EntityCache) Delete(id uint64) error {
key := c.buildKey(id)
ctx := context.Background()
return c.redisClient.Del(ctx, key).Err()
}
func (c *EntityCache) buildKey(id uint64) string {
return fmt.Sprintf("%s%d", cacheKeyPrefix, id)
}
Cache Variants
Boolean flag cache (Set/Get/Delete)
Use when caching simple existence or state flags.
Port (ports/user_activated_cache.go):
type UserActivatedCache interface {
Set(userID uint64) error
Get(userID uint64) (bool, error)
Delete(userID uint64) error
}
Implementation notes:
- Store
"1"as value for true state - Return
false, nilwhen key doesn’t exist (not an error) - Use
errors.Is(err, redisClient.Nil)to detect missing keys
Value cache (Set/Get/Delete with data)
Use when caching structured data or strings.
Port (ports/session_cache.go):
type SessionCache interface {
Set(sessionID string, data SessionData) error
Get(sessionID string) (*SessionData, error)
Delete(sessionID string) error
}
Implementation notes:
- Serialize data with
json.Marshalbefore storing - Deserialize with
json.Unmarshalwhen retrieving - Return
nil, nilwhen key doesn’t exist (not an error) - TTL is internal to the cache implementation with randomized range to prevent cache stampede
Redis Client Usage
The cache uses redis.UniversalClient directly from the Bricks Redis package (github.com/cristiano-pacheco/bricks/pkg/redis).
Common operations:
Set(ctx, key, value, ttl)– Store value with TTLGet(ctx, key)– Retrieve valueDel(ctx, key)– Delete keyExists(ctx, key)– Check if key existsIncr(ctx, key)– Increment counterExpire(ctx, key, ttl)– Set TTL on existing key
Key Building
Always use a helper method to build cache keys consistently:
func (c *EntityCache) buildKey(id uint64) string {
return fmt.Sprintf("%s%d", cacheKeyPrefix, id)
}
For string IDs:
func (c *EntityCache) buildKey(id string) string {
return fmt.Sprintf("%s%s", cacheKeyPrefix, id)
}
For composite keys:
func (c *EntityCache) buildKey(userID uint64, resourceID string) string {
return fmt.Sprintf("%s%d:%s", cacheKeyPrefix, userID, resourceID)
}
TTL Configuration
Define TTL as a range at the package level to prevent cache stampede (multiple entries expiring simultaneously):
const (
cacheKeyPrefix = "entity_name:"
cacheTTLMin = 12 * time.Hour // Minimum TTL
cacheTTLMax = 24 * time.Hour // Maximum TTL
)
Use a helper function to calculate randomized TTL:
import (
"math/rand"
"time"
)
func (c *EntityCache) calculateTTL() time.Duration {
min := cacheTTLMin.Milliseconds()
max := cacheTTLMax.Milliseconds()
randomMs := min + rand.Int63n(max-min+1)
return time.Duration(randomMs) * time.Millisecond
}
Common TTL ranges:
- Short-lived:
4-6 minutes– Rate limits, OTP codes - Session data:
50-70 minutes– User sessions - Daily data:
12-25 hours– User activation status, daily metrics - Weekly data:
6.5-7.5 days– Weekly aggregations
Why randomized TTL? When many cache entries are created at the same time (e.g., during traffic spikes), they would all expire simultaneously, causing a “thundering herd” to the database. Randomizing TTL spreads out expirations over time.
Error Handling
Missing Key vs Error
Distinguish between “key not found” (normal) and actual errors:
result := client.Get(ctx, key)
if err := result.Err(); err != nil {
if errors.Is(err, redisClient.Nil) {
return false, nil // Key doesn't exist - not an error
}
return false, err // Actual error
}
Context Usage
Use context.Background() for cache operations unless you have a specific context:
ctx := context.Background()
For operations called from handlers/use cases, accept context as parameter:
func (c *EntityCache) Set(ctx context.Context, id uint64) error {
key := c.buildKey(id)
// Use provided ctx
return c.redisClient.Set(ctx, key, "1", cacheTTL).Err()
}
Naming
- Port interface:
XxxCache(inportspackage, no suffix) - Implementation struct:
XxxCache(incachepackage, same name â disambiguated by package) - Constructor:
NewXxxCache, returns a pointer of the struct implementation - Constants:
cacheKeyPrefixandcacheTTL(lowercase, package-level)
Fx Wiring
Add to internal/modules/<module>/module.go:
fx.Provide(
fx.Annotate(
cache.NewXxxCache,
fx.As(new(ports.XxxCache)),
),
),
Dependencies
Caches depend on:
redis.UniversalClientfrom"github.com/cristiano-pacheco/bricks/pkg/redis"â Redis operations interface
Example 1: Boolean Flag Cache (User Activation)
Port interface (ports/user_activated_cache.go):
package ports
type UserActivatedCache interface {
Set(userID uint64) error
Get(userID uint64) (bool, error)
Delete(userID uint64) error
}
Implementation (cache/user_activated_cache.go):
package cache
import (
"context"
"errors"
"fmt"
"strconv"
"time"
"github.com/cristiano-pacheco/bricks/pkg/redis"
"github.com/cristiano-pacheco/pingo/internal/modules/identity/ports"
)
const (
cacheKeyPrefix = "user_activated:"
cacheTTLMin = 23 * time.Hour
cacheTTLMax = 25 * time.Hour
)
type UserActivatedCache struct {
redisClient redis.UniversalClient
}
var _ ports.UserActivatedCache = (*UserActivatedCache)(nil)
func NewUserActivatedCache(redisClient redis.UniversalClient) *UserActivatedCache {
return &UserActivatedCache{
redisClient: redisClient,
}
}
func (c *UserActivatedCache) Set(userID uint64) error {
key := c.buildKey(userID)
ctx := context.Background()
ttl := c.calculateTTL()
return c.redisClient.Set(ctx, key, "1", ttl).Err()
}
func (c *UserActivatedCache) calculateTTL() time.Duration {
min := cacheTTLMin.Milliseconds()
max := cacheTTLMax.Milliseconds()
randomMs := min + rand.Int63n(max-min+1)
return time.Duration(randomMs) * time.Millisecond
}
func (c *UserActivatedCache) Get(userID uint64) (bool, error) {
key := c.buildKey(userID)
ctx := context.Background()
result := c.redisClient.Get(ctx, key)
if err := result.Err(); err != nil {
if errors.Is(err, redisClient.Nil) {
return false, nil
}
return false, err
}
return true, nil
}
func (c *UserActivatedCache) Delete(userID uint64) error {
key := c.buildKey(userID)
ctx := context.Background()
return c.redisClient.Del(ctx, key).Err()
}
func (c *UserActivatedCache) buildKey(userID uint64) string {
return fmt.Sprintf("%s%s", cacheKeyPrefix, strconv.FormatUint(userID, 10))
}
Fx wiring (module.go):
fx.Provide(
fx.Annotate(
cache.NewUserActivatedCache,
fx.As(new(ports.UserActivatedCache)),
),
),
Example 2: JSON Data Cache (User Session)
DTO (dto/user_session_dto.go):
package dto
import "time"
type UserSessionData struct {
UserID uint64 `json:"user_id"`
Email string `json:"email"`
Name string `json:"name"`
Roles []string `json:"roles"`
LastActivity time.Time `json:"last_activity"`
IPAddress string `json:"ip_address"`
}
Port interface (ports/user_session_cache.go):
package ports
import (
"time"
"github.com/cristiano-pacheco/pingo/internal/modules/identity/dto"
)
type UserSessionCache interface {
Set(sessionID string, data dto.UserSessionData) error
Get(sessionID string) (*dto.UserSessionData, error)
Delete(sessionID string) error
Exists(sessionID string) (bool, error)
}
Implementation (cache/user_session_cache.go):
package cache
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/cristiano-pacheco/bricks/pkg/redis"
"github.com/cristiano-pacheco/pingo/internal/modules/identity/dto"
"github.com/cristiano-pacheco/pingo/internal/modules/identity/ports"
)
const (
sessionCacheKeyPrefix = "user_session:"
sessionCacheTTLMin = 50 * time.Minute
sessionCacheTTLMax = 70 * time.Minute
)
type UserSessionCache struct {
redisClient redis.UniversalClient
}
var _ ports.UserSessionCache = (*UserSessionCache)(nil)
func NewUserSessionCache(redisClient redis.UniversalClient) *UserSessionCache {
return &UserSessionCache{
redisClient: redisClient,
}
}
func (c *UserSessionCache) Set(sessionID string, data dto.UserSessionData) error {
key := c.buildKey(sessionID)
ctx := context.Background()
jsonData, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("failed to marshal session data: %w", err)
}
ttl := c.calculateTTL()
return c.redisClient.Set(ctx, key, jsonData, ttl).Err()
}
func (c *UserSessionCache) calculateTTL() time.Duration {
min := sessionCacheTTLMin.Milliseconds()
max := sessionCacheTTLMax.Milliseconds()
randomMs := min + rand.Int63n(max-min+1)
return time.Duration(randomMs) * time.Millisecond
}
func (c *UserSessionCache) Get(sessionID string) (*dto.UserSessionData, error) {
key := c.buildKey(sessionID)
ctx := context.Background()
result := c.redisClient.Get(ctx, key)
if err := result.Err(); err != nil {
if errors.Is(err, redisClient.Nil) {
return nil, nil
}
return nil, err
}
jsonData, err := result.Bytes()
if err != nil {
return nil, fmt.Errorf("failed to get bytes: %w", err)
}
var data dto.UserSessionData
if err := json.Unmarshal(jsonData, &data); err != nil {
return nil, fmt.Errorf("failed to unmarshal session data: %w", err)
}
return &data, nil
}
func (c *UserSessionCache) Delete(sessionID string) error {
key := c.buildKey(sessionID)
ctx := context.Background()
return c.redisClient.Del(ctx, key).Err()
}
func (c *UserSessionCache) Exists(sessionID string) (bool, error) {
key := c.buildKey(sessionID)
ctx := context.Background()
result := c.redisClient.Exists(ctx, key)
if err := result.Err(); err != nil {
return false, err
}
return result.Val() > 0, nil
}
func (c *UserSessionCache) buildKey(sessionID string) string {
return fmt.Sprintf("%s%s", sessionCacheKeyPrefix, sessionID)
}
Fx wiring (module.go):
fx.Provide(
fx.Annotate(
cache.NewUserSessionCache,
fx.As(new(ports.UserSessionCache)),
),
),
Example 3: Protobuf Data Cache (User Profile)
Proto definition (proto/user_profile.proto):
syntax = "proto3";
package identity;
option go_package = "github.com/cristiano-pacheco/pingo/internal/modules/identity/proto";
message UserProfile {
uint64 user_id = 1;
string email = 2;
string name = 3;
repeated string roles = 4;
int64 last_login = 5;
string avatar_url = 6;
}
Port interface (ports/user_profile_cache.go):
package ports
import (
"time"
"github.com/cristiano-pacheco/pingo/internal/modules/identity/proto"
)
type UserProfileCache interface {
Set(userID uint64, profile *proto.UserProfile) error
Get(userID uint64) (*proto.UserProfile, error)
Delete(userID uint64) error
}
Implementation (cache/user_profile_cache.go):
package cache
import (
"context"
"errors"
"fmt"
"time"
"github.com/cristiano-pacheco/bricks/pkg/redis"
"github.com/cristiano-pacheco/pingo/internal/modules/identity/ports"
"github.com/cristiano-pacheco/pingo/internal/modules/identity/proto"
"google.golang.org/protobuf/proto"
)
const (
profileCacheKeyPrefix = "user_profile:"
profileCacheTTLMin = 12 * time.Hour
profileCacheTTLMax = 24 * time.Hour
)
type UserProfileCache struct {
redisClient redis.UniversalClient
}
var _ ports.UserProfileCache = (*UserProfileCache)(nil)
func NewUserProfileCache(redisClient redis.UniversalClient) *UserProfileCache {
return &UserProfileCache{
redisClient: redisClient,
}
}
func (c *UserProfileCache) Set(userID uint64, profile *proto.UserProfile) error {
key := c.buildKey(userID)
ctx := context.Background()
data, err := proto.Marshal(profile)
if err != nil {
return fmt.Errorf("failed to marshal profile: %w", err)
}
ttl := c.calculateTTL()
return c.redisClient.Set(ctx, key, data, ttl).Err()
}
func (c *UserProfileCache) calculateTTL() time.Duration {
min := profileCacheTTLMin.Milliseconds()
max := profileCacheTTLMax.Milliseconds()
randomMs := min + rand.Int63n(max-min+1)
return time.Duration(randomMs) * time.Millisecond
}
func (c *UserProfileCache) Get(userID uint64) (*proto.UserProfile, error) {
key := c.buildKey(userID)
ctx := context.Background()
result := c.redisClient.Get(ctx, key)
if err := result.Err(); err != nil {
if errors.Is(err, redisClient.Nil) {
return nil, nil
}
return nil, err
}
data, err := result.Bytes()
if err != nil {
return nil, fmt.Errorf("failed to get bytes: %w", err)
}
var profile proto.UserProfile
if err := proto.Unmarshal(data, &profile); err != nil {
return nil, fmt.Errorf("failed to unmarshal profile: %w", err)
}
return &profile, nil
}
func (c *UserProfileCache) Delete(userID uint64) error {
key := c.buildKey(userID)
ctx := context.Background()
return c.redisClient.Del(ctx, key).Err()
}
func (c *UserProfileCache) buildKey(userID uint64) string {
return fmt.Sprintf("%s%d", profileCacheKeyPrefix, userID)
}
Fx wiring (module.go):
fx.Provide(
fx.Annotate(
cache.NewUserProfileCache,
fx.As(new(ports.UserProfileCache)),
),
),
Critical Rules
- Two files: Port interface in
ports/, implementation incache/ - Interface in ports: Interface lives in
ports/<name>_cache.go - Interface assertion: Add
var _ ports.XxxCache = (*XxxCache)(nil)below the struct - Constructor: MUST return pointer
*XxxCache - Constants: Define
cacheKeyPrefix,cacheTTLMin, andcacheTTLMaxat package level - Randomized TTL: MUST use
calculateTTL()helper to prevent cache stampede - Key builder: Always use a
buildKey()helper method - Missing keys: Return zero value + nil error, not an error (use
errors.Is(err, redisClient.Nil)) - Context: Use
context.Background()or acceptcontext.Contextparameter - No comments: Do not add redundant comments above methods
- Add detailed comment on interfaces: Provide comprehensive comments on the port interfaces to describe their purpose and usage
- Redis client type: Use
redis.UniversalClientinterface - No TTL parameters: TTL is internal to cache, never exposed in interface methods
Workflow
- Create port interface in
ports/<name>_cache.go - Create cache implementation in
cache/<name>_cache.go - Add Fx wiring to module’s
module.go - Run
make lintto verify - Run
make nilawayfor static analysis