延迟加载必须基于接口:调用方只依赖接口(如DataLoader),而非具体类型;proxy结构体字段须为nil指针,配合sync.Once和错误缓存实现安全懒初始化。

延迟加载必须基于接口,不能直接操作具体类型
Go 没有语言级代理,延迟加载生效的前提是调用方只依赖接口,而不是具体结构体。一旦代码里直接 new 一个 RealService{} 或传入 *RealService,后续再套 proxy 就失去意义——对象早已初始化完毕。
常见错误是把延迟逻辑写在构造函数里,比如:NewProxyService() 内部就初始化了真实对象。这等于把“懒”字擦掉了。
- 定义统一接口,例如
DataLoader、CacheClient - 真实实现(
RealLoader)和代理(ProxyLoader)都实现该接口 - 所有上层代码只接收
DataLoader类型参数,不关心背后是哪个 struct - DI 容器或工厂函数返回接口值,而非具体类型指针
接口要足够窄,避免 proxy 暴露不该有的能力
如果一个接口定义了 10 个方法,但某类用户只允许调用其中 2 个,硬是在 ProxyLoader 的每个方法里加 if !user.Can("read") 判断,既难维护又破坏封装。更糟的是,编译器不会提醒你误调用了被禁止的方法。
真正可控的做法是拆分接口:
-
Reader接口只含Get(key string) (any, error) -
Writer接口只含Set(key string, val any) error -
AdminOps接口含FlushAll()、Stats()等高危操作 - 不同角色的 proxy 只实现对应接口,比如
ReadOnlyProxy不实现Writer,编译期就报错
proxy 结构体字段必须为指针且初始为 nil
这是延迟加载能否触发的关键。如果 ProxyLoader 里定义的是 real RealLoader(值类型)或 real *RealLoader = &RealLoader{}(非 nil 初始化),Load() 方法第一次调用时根本不会走初始化分支。
正确写法是:
type ProxyLoader struct {
real *RealLoader
once sync.Once
}
并在 Load() 中检查 p.real == nil 后才调用 p.once.Do(p.init)。注意:这个 init 方法里别做阻塞操作(如读大文件),否则首次调用会被卡住。
并发场景下 init 失败要缓存错误,否则反复重试
多个 goroutine 同时触发 Load(),若 init 出错(比如数据库连不上),不保存错误会导致每次调用都重试——既浪费资源,又可能触发下游告警风暴。
推荐模式:
- 加一个
initErr error字段 -
init方法中出错时赋值:p.initErr = err - 主方法开头先判断:
if p.initErr != nil { return nil, p.initErr } - 成功后才设置
p.real = &RealLoader{...}
这种模式在 Agollo 的 GetConfigCache 和 Viper 的某些 lazy config 加载路径里都能看到影子——它不是“懒”,而是“带状态的懒”。容易忽略的是:错误缓存后,除非显式重试机制(如 lazy.Group),否则失败状态会一直持续。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











