Go中代理模式需基于接口实现,权限校验必须前置且立即返回,缓存要区分读写、防穿透击穿,代理链不超过两层,构造函数应接收依赖而非全局变量,并确保线程安全。

Go 本身没有内置的“代理模式”语法支持,但可以通过接口 + 结构体组合 + 委托调用清晰实现。关键不在于“怎么写代理”,而在于「什么时候该用」以及「权限和缓存逻辑放哪一层才不破坏单一职责」。
代理对象必须基于接口,否则无法替换真实实现
Go 的代理依赖鸭子类型,真实对象和代理对象必须实现同一接口。如果直接代理具体类型(如 *UserService),就丧失了运行时替换能力,后续加权限或缓存都会卡死。
实操建议:
- 先定义接口(如
UserRepository),再让真实结构体(DBUserRepo)和代理结构体(CachedUserRepo)都实现它 - 代理结构体内部持有一个该接口类型的字段(
repo UserRepository),而非具体类型 - 避免在代理中暴露真实对象的字段或方法——那等于把门钥匙交出去了
权限校验应放在代理的入口,且失败时立即返回
权限不是装饰器式的后置钩子,而是前置守门员。代理收到请求后第一件事就是检查 ctx.Value("user_role") 或解析 token,不通过就直接 return nil, errors.New("permission denied"),绝不往下委托。
常见错误现象:
- 在委托调用后才检查返回结果是否越权(比如查到了别人的数据再过滤)——已造成数据泄露
- 把权限逻辑写在真实对象里,导致无法统一管控,测试和灰度困难
- 用 panic 做权限拦截——Go 中 panic 应只用于真正异常,不该用于业务控制流
缓存逻辑要区分读写,且避免缓存穿透与击穿
代理做缓存,不能简单地「查缓存 → 没有就查 DB → 写回缓存」。真实场景中,GetUserByID 可缓存,但 UpdateUser 必须清缓存,否则脏数据;空结果也要缓存(防穿透),过期时间得比正常值短(防击穿)。
实操要点:
- 读操作:先查
redis.Get("user:123"),命中则反序列化返回;未命中则调用p.repo.GetUserByID(),若返回非 nil 则redis.Set("user:123", val, 5*time.Minute);若返回 nil,设一个空值缓存(如"null")并配 30 秒 TTL - 写操作:执行
p.repo.UpdateUser()后,同步redis.Del("user:123"),不要等 TTL 过期 - 避免用
sync.Map做跨请求缓存——它不持久、不共享、无淘汰策略,仅适合临时本地加速
代理链可以嵌套,但别超过两层
你可以让 CachedUserRepo 包裹 AuthUserRepo,再包裹 DBUserRepo,这样权限和缓存解耦。但三层以上(比如再加个日志代理、指标代理)会让调用栈变深、错误堆栈难读、性能损耗明显。
更务实的做法是:
- 用一个代理结构体同时聚合权限与缓存逻辑(只要它们不互相干扰)
- 把日志、trace、metrics 等横切关注点交给中间件(如 HTTP handler 链)或专用 instrumentation 包,别塞进业务代理里
- 所有代理的构造函数接受
context.Context或配置对象,而不是全局变量——否则单元测试时根本没法 mock
最易被忽略的一点:代理对象本身不是线程安全的,如果你在多个 goroutine 中复用同一个代理实例,又没对内部缓存(如 map)加锁,就会出现 data race。别假设“我只读不写”——缓存更新就是写。要么用 sync.RWMutex,要么换用线程安全的缓存库(如 github.com/patrickmn/go-cache)。










