go-create-cache
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseGo Create Cache
Go 缓存实现创建指南
Generate cache files for Go backend using Redis.
为Go后端生成基于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
每个缓存都需要两个文件:
- 端口接口:
internal/modules/<module>/ports/<cache_name>_cache.go - 缓存实现:
internal/modules/<module>/cache/<cache_name>_cache.go
Port File Layout Order
端口文件结构顺序
- Interface definition (— no suffix)
XxxCache
- 接口定义(命名为,无后缀)
XxxCache
Cache File Layout Order
缓存文件结构顺序
- Constants (cache key prefix, TTL)
- Implementation struct ()
XxxCache - Compile-time interface assertion
- Constructor ()
NewXxxCache - Methods (,
Set,Get, etc.)Delete - Helper methods (, etc.)
buildKey
- 常量(缓存键前缀、TTL)
- 实现结构体()
XxxCache - 编译时接口断言
- 构造函数()
NewXxxCache - 方法(、
Set、Get等)Delete - 辅助方法(等)
buildKey
Port Interface Structure
端口接口结构
Location:
internal/modules/<module>/ports/<cache_name>_cache.gogo
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.gogo
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.gogo
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.gogo
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)
布尔标记缓存(Set/Get/Delete)
Use when caching simple existence or state flags.
Port ():
ports/user_activated_cache.gogo
type UserActivatedCache interface {
Set(userID uint64) error
Get(userID uint64) (bool, error)
Delete(userID uint64) error
}Implementation notes:
- Store as value for true state
"1" - Return when key doesn't exist (not an error)
false, nil - Use to detect missing keys
errors.Is(err, redisClient.Nil)
适用于缓存简单的存在性或状态标记场景。
端口文件():
ports/user_activated_cache.gogo
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.gogo
type SessionCache interface {
Set(sessionID string, data SessionData) error
Get(sessionID string) (*SessionData, error)
Delete(sessionID string) error
}Implementation notes:
- Serialize data with before storing
json.Marshal - Deserialize with when retrieving
json.Unmarshal - Return when key doesn't exist (not an error)
nil, nil - TTL is internal to the cache implementation with randomized range to prevent cache stampede
适用于缓存结构化数据或字符串的场景。
端口文件():
ports/session_cache.gogo
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 directly from the Bricks Redis package ().
redis.UniversalClientgithub.com/cristiano-pacheco/bricks/pkg/redisCommon operations:
- - Store value with TTL
Set(ctx, key, value, ttl) - - Retrieve value
Get(ctx, key) - - Delete key
Del(ctx, key) - - Check if key exists
Exists(ctx, key) - - Increment counter
Incr(ctx, key) - - Set TTL on existing key
Expire(ctx, key, ttl)
缓存直接使用Bricks Redis包()中的。
github.com/cristiano-pacheco/bricks/pkg/redisredis.UniversalClient常用操作:
- - 存储带TTL的值
Set(ctx, key, value, ttl) - - 获取值
Get(ctx, key) - - 删除键
Del(ctx, key) - - 检查键是否存在
Exists(ctx, key) - - 递增计数器
Incr(ctx, key) - - 为已有键设置TTL
Expire(ctx, key, 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: - Rate limits, OTP codes
4-6 minutes - Session data: - User sessions
50-70 minutes - Daily data: - User activation status, daily metrics
12-25 hours - Weekly data: - Weekly aggregations
6.5-7.5 days
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范围:
- 短时效:- 限流、OTP验证码
4-6分钟 - 会话数据:- 用户会话
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 for cache operations unless you have a specific context:
context.Background()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: (in
XxxCachepackage, no suffix)ports - Implementation struct: (in
XxxCachepackage, same name — disambiguated by package)cache - Constructor: , returns a pointer of the struct implementation
NewXxxCache - Constants: and
cacheKeyPrefix(lowercase, package-level)cacheTTL
- 端口接口:(位于
XxxCache包,无后缀)ports - 实现结构体:(位于
XxxCache包,同名——通过包名区分)cache - 构造函数:,返回结构体实现的指针
NewXxxCache - 常量:和
cacheKeyPrefix(小写,包级别)cacheTTL
Fx Wiring
Fx依赖注入配置
Add to :
internal/modules/<module>/module.gogo
fx.Provide(
fx.Annotate(
cache.NewXxxCache,
fx.As(new(ports.XxxCache)),
),
),添加到:
internal/modules/<module>/module.gogo
fx.Provide(
fx.Annotate(
cache.NewXxxCache,
fx.As(new(ports.XxxCache)),
),
),Dependencies
依赖项
Caches depend on:
- from
redis.UniversalClient— Redis operations interface"github.com/cristiano-pacheco/bricks/pkg/redis"
缓存依赖于:
- (来自
redis.UniversalClient)——Redis操作接口"github.com/cristiano-pacheco/bricks/pkg/redis"
Example 1: Boolean Flag Cache (User Activation)
示例1:布尔标记缓存(用户激活状态)
Port interface ():
ports/user_activated_cache.gogo
package ports
type UserActivatedCache interface {
Set(userID uint64) error
Get(userID uint64) (bool, error)
Delete(userID uint64) error
}Implementation ():
cache/user_activated_cache.gogo
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.gogo
fx.Provide(
fx.Annotate(
cache.NewUserActivatedCache,
fx.As(new(ports.UserActivatedCache)),
),
),端口接口():
ports/user_activated_cache.gogo
package ports
type UserActivatedCache interface {
Set(userID uint64) error
Get(userID uint64) (bool, error)
Delete(userID uint64) error
}实现代码():
cache/user_activated_cache.gogo
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.gogo
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.gogo
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.gogo
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.gogo
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.gogo
fx.Provide(
fx.Annotate(
cache.NewUserSessionCache,
fx.As(new(ports.UserSessionCache)),
),
),DTO():
dto/user_session_dto.gogo
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.gogo
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.gogo
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.gogo
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.protoprotobuf
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.gogo
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.gogo
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.gogo
fx.Provide(
fx.Annotate(
cache.NewUserProfileCache,
fx.As(new(ports.UserProfileCache)),
),
),Proto定义():
proto/user_profile.protoprotobuf
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.gogo
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.gogo
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.gogo
fx.Provide(
fx.Annotate(
cache.NewUserProfileCache,
fx.As(new(ports.UserProfileCache)),
),
),Critical Rules
核心规则
- Two files: Port interface in , implementation in
ports/cache/ - Interface in ports: Interface lives in
ports/<name>_cache.go - Interface assertion: Add below the struct
var _ ports.XxxCache = (*XxxCache)(nil) - Constructor: MUST return pointer
*XxxCache - Constants: Define ,
cacheKeyPrefix, andcacheTTLMinat package levelcacheTTLMax - Randomized TTL: MUST use helper to prevent cache stampede
calculateTTL() - Key builder: Always use a helper method
buildKey() - Missing keys: Return zero value + nil error, not an error (use )
errors.Is(err, redisClient.Nil) - Context: Use or accept
context.Background()parametercontext.Context - No comments: Do not add redundant comments above methods
- Redis client type: Use interface
redis.UniversalClient - No TTL parameters: TTL is internal to cache, never exposed in interface methods
- 双文件结构:端口接口位于目录,实现代码位于
ports/目录cache/ - 接口存放位置:接口定义在中
ports/<name>_cache.go - 接口断言:在结构体下方添加
var _ ports.XxxCache = (*XxxCache)(nil) - 构造函数:必须返回指针
*XxxCache - 常量定义:在包级别定义、
cacheKeyPrefix和cacheTTLMincacheTTLMax - 随机TTL:必须使用辅助函数以避免缓存雪崩
calculateTTL() - 键构建:始终使用辅助方法
buildKey() - 键缺失处理:返回零值+nil错误,而非错误(使用)
errors.Is(err, redisClient.Nil) - Context使用:使用或接受
context.Background()参数context.Context - 避免冗余注释:不要在方法上方添加多余注释
- Redis客户端类型:使用接口
redis.UniversalClient - TTL不对外暴露:TTL由缓存内部管理,绝不暴露在接口方法中
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 to verify
make lint - Run for static analysis
make nilaway
- 在中创建端口接口
ports/<name>_cache.go - 在中创建缓存实现
cache/<name>_cache.go - 在模块的中添加Fx配置
module.go - 运行进行验证
make lint - 运行进行静态分析
make nilaway