延迟加载要求字段初始化为nil,首次调用业务方法时通过sync.once执行含错误返回的init函数,并将错误存为字段供后续调用透传;不可在构造时实例化对象或静默吞错。

延迟加载本身不返回对象和错误,真正返回的是你封装的加载方法——它必须显式声明 (*T, error) 签名,并在首次调用时完成初始化与错误传播。
为什么不能把 new(T) 直接写在 struct 字段里
常见错误是这么写:type Proxy struct { real *RealService },然后在构造函数里 p.real = &RealService{}。这根本不是延迟加载,对象在 proxy 创建时就已实例化,且无法捕获初始化失败(比如依赖未就绪、配置缺失)。
- 延迟加载的核心动作发生在“第一次调用业务方法时”,不是构造时
- 字段必须初始化为
nil,否则if p.real == nil永远不成立 - 如果初始化涉及 I/O 或网络,失败必须能被调用方感知,不能静默吞掉
sync.Once 是最简可靠的初始化同步机制
并发场景下,多个 goroutine 同时触发首次加载,sync.Once 能确保 init 只执行一次,且所有等待者会拿到同一结果(无论成功或失败)。
- 不要自己手写双检锁:容易漏掉
mu.RLock()或忘记在检查后加锁 -
Once.Do()内部已处理 panic 捕获,但你的init函数仍需返回error并存到字段中 - 示例签名:
func (p *Proxy) GetService() (*RealService, error) { p.once.Do(p.init); return p.real, p.err }
初始化失败时如何让错误透传给调用方
如果 init 失败(如连接数据库超时),不能只记日志然后忽略——调用方需要明确知道“对象不可用”,而不是拿到一个 nil 指针再 panic。
- 必须把错误保存为 proxy 的字段:
p.err = fmt.Errorf("connect db failed: %w", err) - 后续每次调用
GetService()都要先检查p.err != nil,直接返回该错误 - 不要在
init里log.Fatal或os.Exit——这会让整个服务崩溃,违背延迟加载的容错初衷 - 若需重试,
sync.Once不适用,得换lazy.Group或自定义带状态的懒加载器
真正难的不是写 once.Do,而是决定“失败后要不要重试”“要不要降级返回默认实现”“错误是否该包含上下文堆栈”。这些逻辑不会自动出现在 proxy 里,得你一条条写清楚。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











