go-cache

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Go Cache

Go 缓存

Generate cache files for Go backend using Redis.
为Go后端生成基于Redis的缓存文件。

Two-File Pattern

双文件模式

Every cache requires two files:
  1. Port interface:
    internal/modules/<module>/ports/<cache_name>_cache.go
  2. Cache implementation:
    internal/modules/<module>/cache/<cache_name>_cache.go
每个缓存都需要两个文件:
  1. 端口接口
    internal/modules/<module>/ports/<cache_name>_cache.go
  2. 缓存实现
    internal/modules/<module>/cache/<cache_name>_cache.go

Port File Layout Order

端口文件布局顺序

  1. Interface definition (
    XxxCache
    — no suffix)
  1. 接口定义(
    XxxCache
    — 无后缀)

Cache File Layout Order

缓存文件布局顺序

  1. Constants (cache key prefix, TTL)
  2. Implementation struct (
    XxxCache
    )
  3. Compile-time interface assertion
  4. Constructor (
    NewXxxCache
    )
  5. Methods (
    Set
    ,
    Get
    ,
    Delete
    , etc.)
  6. Helper methods (
    buildKey
    , etc.)
  1. 常量(缓存键前缀、TTL)
  2. 实现结构体(
    XxxCache
  3. 编译期接口断言
  4. 构造函数(
    NewXxxCache
  5. 方法(
    Set
    Get
    Delete
    等)
  6. 辅助方法(
    buildKey
    等)

Port Interface Structure

端口接口结构

Location:
internal/modules/<module>/ports/<cache_name>_cache.go
go
package ports

type UserActivatedCache interface {
	Set(userID uint64) error
	Get(userID uint64) (bool, error)
	Delete(userID uint64) error
}
位置
internal/modules/<module>/ports/<cache_name>_cache.go
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
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)
}
位置
internal/modules/<module>/cache/<cache_name>_cache.go
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 // 键不存在
		}
		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)

布尔标记缓存(Set/Get/Delete)

Use when caching simple existence or state flags.
Port (
ports/user_activated_cache.go
):
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, nil
    when key doesn't exist (not an error)
  • Use
    errors.Is(err, redisClient.Nil)
    to detect missing keys
适用于缓存简单的存在性或状态标记。
端口文件(
ports/user_activated_cache.go
):
go
type UserActivatedCache interface {
	Set(userID uint64) error
	Get(userID uint64) (bool, error)
	Delete(userID uint64) error
}
实现注意事项:
  • 存储
    "1"
    表示真状态
  • 当键不存在时返回
    false, nil
    (不视为错误)
  • 使用
    errors.Is(err, redisClient.Nil)
    检测缺失的键

Value cache (Set/Get/Delete with data)

值缓存(带数据的Set/Get/Delete)

Use when caching structured data or strings.
Port (
ports/session_cache.go
):
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.Marshal
    before storing
  • Deserialize with
    json.Unmarshal
    when retrieving
  • Return
    nil, nil
    when key doesn't exist (not an error)
  • TTL is internal to the cache implementation with randomized range to prevent cache stampede
适用于缓存结构化数据或字符串。
端口文件(
ports/session_cache.go
):
go
type SessionCache interface {
	Set(sessionID string, data SessionData) error
	Get(sessionID string) (*SessionData, error)
	Delete(sessionID string) error
}
实现注意事项:
  • 存储前使用
    json.Marshal
    序列化数据
  • 读取时使用
    json.Unmarshal
    反序列化
  • 当键不存在时返回
    nil, nil
    (不视为错误)
  • TTL在缓存实现内部通过随机范围设置,防止缓存雪崩

Redis Client Usage

Redis客户端使用

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 TTL
  • Get(ctx, key)
    - Retrieve value
  • Del(ctx, key)
    - Delete key
  • Exists(ctx, key)
    - Check if key exists
  • Incr(ctx, key)
    - Increment counter
  • Expire(ctx, key, ttl)
    - Set TTL on existing key
缓存直接使用Bricks Redis包(
github.com/cristiano-pacheco/bricks/pkg/redis
)中的
redis.UniversalClient
常见操作:
  • Set(ctx, key, value, ttl)
    - 存储带TTL的值
  • Get(ctx, key)
    - 获取值
  • Del(ctx, key)
    - 删除键
  • Exists(ctx, key)
    - 检查键是否存在
  • Incr(ctx, key)
    - 递增计数器
  • Expire(ctx, key, ttl)
    - 为已有键设置TTL

Key Building

键构建

Always use a helper method to build cache keys consistently:
go
func (c *EntityCache) buildKey(id uint64) string {
	return fmt.Sprintf("%s%d", cacheKeyPrefix, id)
}
For string IDs:
go
func (c *EntityCache) buildKey(id string) string {
	return fmt.Sprintf("%s%s", cacheKeyPrefix, id)
}
For composite keys:
go
func (c *EntityCache) buildKey(userID uint64, resourceID string) string {
	return fmt.Sprintf("%s%d:%s", cacheKeyPrefix, userID, resourceID)
}
始终使用辅助方法来统一构建缓存键:
go
func (c *EntityCache) buildKey(id uint64) string {
	return fmt.Sprintf("%s%d", cacheKeyPrefix, id)
}
对于字符串类型ID:
go
func (c *EntityCache) buildKey(id string) string {
	return fmt.Sprintf("%s%s", cacheKeyPrefix, id)
}
对于复合键:
go
func (c *EntityCache) buildKey(userID uint64, resourceID string) string {
	return fmt.Sprintf("%s%d:%s", cacheKeyPrefix, userID, resourceID)
}

TTL Configuration

TTL配置

Define TTL as a range at the package level to prevent cache stampede (multiple entries expiring simultaneously):
go
const (
	cacheKeyPrefix    = "entity_name:"
	cacheTTLMin       = 12 * time.Hour  // Minimum TTL
	cacheTTLMax       = 24 * time.Hour  // Maximum TTL
)
Use a helper function to calculate randomized TTL:
go
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.
在包级别定义TTL范围以防止缓存雪崩(多个条目同时过期):
go
const (
	cacheKeyPrefix    = "entity_name:"
	cacheTTLMin       = 12 * time.Hour  // 最小TTL
	cacheTTLMax       = 24 * time.Hour  // 最大TTL
)
使用辅助函数计算随机TTL:
go
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
}
常见TTL范围:
  • 短时效:
    4-6分钟
    - 限流、OTP验证码
  • 会话数据:
    50-70分钟
    - 用户会话
  • 每日数据:
    12-25小时
    - 用户激活状态、每日指标
  • 每周数据:
    6.5-7.5天
    - 周度聚合数据
为什么使用随机TTL? 当大量缓存条目同时创建时(例如流量峰值期间),它们会同时过期,导致对数据库的“惊群效应”。随机TTL可以将过期时间分散到不同时段。

Error Handling

错误处理

Missing Key vs Error

缺失键 vs 错误

Distinguish between "key not found" (normal) and actual errors:
go
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
}
区分“键不存在”(正常情况)和实际错误:
go
result := client.Get(ctx, key)
if err := result.Err(); err != nil {
	if errors.Is(err, redisClient.Nil) {
		return false, nil // 键不存在 - 不是错误
	}
	return false, err // 实际错误
}

Context Usage

Context使用

Use
context.Background()
for cache operations unless you have a specific context:
go
ctx := context.Background()
For operations called from handlers/use cases, accept context as parameter:
go
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()
}
除非有特定上下文,否则缓存操作使用
context.Background()
go
ctx := context.Background()
对于从处理器/用例中调用的操作,接受context作为参数:
go
func (c *EntityCache) Set(ctx context.Context, id uint64) error {
	key := c.buildKey(id)
	// 使用传入的ctx
	return c.redisClient.Set(ctx, key, "1", cacheTTL).Err()
}

Naming

命名规范

  • Port interface:
    XxxCache
    (in
    ports
    package, no suffix)
  • Implementation struct:
    XxxCache
    (in
    cache
    package, same name — disambiguated by package)
  • Constructor:
    NewXxxCache
    , returns a pointer of the struct implementation
  • Constants:
    cacheKeyPrefix
    and
    cacheTTL
    (lowercase, package-level)
  • 端口接口:
    XxxCache
    (位于
    ports
    包,无后缀)
  • 实现结构体:
    XxxCache
    (位于
    cache
    包,同名——通过包名区分)
  • 构造函数:
    NewXxxCache
    ,返回结构体指针
  • 常量:
    cacheKeyPrefix
    cacheTTL
    (小写,包级别)

Fx Wiring

Fx 依赖注入配置

Add to
internal/modules/<module>/module.go
:
go
fx.Provide(
	fx.Annotate(
		cache.NewXxxCache,
		fx.As(new(ports.XxxCache)),
	),
),
添加到
internal/modules/<module>/module.go
go
fx.Provide(
	fx.Annotate(
		cache.NewXxxCache,
		fx.As(new(ports.XxxCache)),
	),
),

Dependencies

依赖项

Caches depend on:
  • redis.UniversalClient
    from
    "github.com/cristiano-pacheco/bricks/pkg/redis"
    — Redis operations interface
缓存依赖于:
  • redis.UniversalClient
    (来自
    "github.com/cristiano-pacheco/bricks/pkg/redis"
    )——Redis操作接口

Example 1: Boolean Flag Cache (User Activation)

示例1:布尔标记缓存(用户激活状态)

Port interface (
ports/user_activated_cache.go
):
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
):
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
):
go
fx.Provide(
	fx.Annotate(
		cache.NewUserActivatedCache,
		fx.As(new(ports.UserActivatedCache)),
	),
),
端口接口(
ports/user_activated_cache.go
):
go
package ports

type UserActivatedCache interface {
	Set(userID uint64) error
	Get(userID uint64) (bool, error)
	Delete(userID uint64) error
}
实现(
cache/user_activated_cache.go
):
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配置(
module.go
):
go
fx.Provide(
	fx.Annotate(
		cache.NewUserActivatedCache,
		fx.As(new(ports.UserActivatedCache)),
	),
),

Example 2: JSON Data Cache (User Session)

示例2:JSON数据缓存(用户会话)

DTO (
dto/user_session_dto.go
):
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
):
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
):
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
):
go
fx.Provide(
	fx.Annotate(
		cache.NewUserSessionCache,
		fx.As(new(ports.UserSessionCache)),
	),
),
DTO(
dto/user_session_dto.go
):
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"`
}
端口接口(
ports/user_session_cache.go
):
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)
}
实现(
cache/user_session_cache.go
):
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配置(
module.go
):
go
fx.Provide(
	fx.Annotate(
		cache.NewUserSessionCache,
		fx.As(new(ports.UserSessionCache)),
	),
),

Example 3: Protobuf Data Cache (User Profile)

示例3:Protobuf数据缓存(用户资料)

Proto definition (
proto/user_profile.proto
):
protobuf
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
):
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
):
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
):
go
fx.Provide(
	fx.Annotate(
		cache.NewUserProfileCache,
		fx.As(new(ports.UserProfileCache)),
	),
),
Proto定义(
proto/user_profile.proto
):
protobuf
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;
}
端口接口(
ports/user_profile_cache.go
):
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
}
实现(
cache/user_profile_cache.go
):
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配置(
module.go
):
go
fx.Provide(
	fx.Annotate(
		cache.NewUserProfileCache,
		fx.As(new(ports.UserProfileCache)),
	),
),

Critical Rules

关键规则

  1. Two files: Port interface in
    ports/
    , implementation in
    cache/
  2. Interface in ports: Interface lives in
    ports/<name>_cache.go
  3. Interface assertion: Add
    var _ ports.XxxCache = (*XxxCache)(nil)
    below the struct
  4. Constructor: MUST return pointer
    *XxxCache
  5. Constants: Define
    cacheKeyPrefix
    ,
    cacheTTLMin
    , and
    cacheTTLMax
    at package level
  6. Randomized TTL: MUST use
    calculateTTL()
    helper to prevent cache stampede
  7. Key builder: Always use a
    buildKey()
    helper method
  8. Missing keys: Return zero value + nil error, not an error (use
    errors.Is(err, redisClient.Nil)
    )
  9. Context: Use
    context.Background()
    or accept
    context.Context
    parameter
  10. No comments: Do not add redundant comments above methods
  11. Add detailed comment on interfaces: Provide comprehensive comments on the port interfaces to describe their purpose and usage
  12. Redis client type: Use
    redis.UniversalClient
    interface
  13. No TTL parameters: TTL is internal to cache, never exposed in interface methods
  1. 双文件:端口接口位于
    ports/
    ,实现位于
    cache/
  2. 接口位置:接口存放在
    ports/<name>_cache.go
  3. 接口断言:在结构体下方添加
    var _ ports.XxxCache = (*XxxCache)(nil)
  4. 构造函数:必须返回指针
    *XxxCache
  5. 常量:在包级别定义
    cacheKeyPrefix
    cacheTTLMin
    cacheTTLMax
  6. 随机TTL:必须使用
    calculateTTL()
    辅助函数防止缓存雪崩
  7. 键构建器:始终使用
    buildKey()
    辅助方法
  8. 缺失键处理:返回零值+nil错误,而非错误(使用
    errors.Is(err, redisClient.Nil)
  9. Context:使用
    context.Background()
    或接受
    context.Context
    参数
  10. 无冗余注释:不要在方法上方添加冗余注释
  11. 接口详细注释:为端口接口添加全面注释,描述其用途和使用场景
  12. Redis客户端类型:使用
    redis.UniversalClient
    接口
  13. 无TTL参数:TTL是缓存内部实现,永远不要在接口方法中暴露

Workflow

工作流程

  1. Create port interface in
    ports/<name>_cache.go
  2. Create cache implementation in
    cache/<name>_cache.go
  3. Add Fx wiring to module's
    module.go
  4. Run
    make lint
    to verify
  5. Run
    make nilaway
    for static analysis
  1. ports/<name>_cache.go
    创建端口接口
  2. cache/<name>_cache.go
    创建缓存实现
  3. 在模块的
    module.go
    中添加Fx配置
  4. 运行
    make lint
    验证
  5. 运行
    make nilaway
    进行静态分析