Go代理模式必须通过定义接口、组合真实对象、显式委托实现;不抽接口、不显式调用即非代理,仅是普通包装。

Go 里没有“自动代理”,所有代理都得手写接口 + 组合 + 显式委托;不抽接口、不显式调用真实对象,就不是代理,只是普通包装。
必须先定义 interface,不能直接代理 struct
常见错误是给一个 UserService struct 直接加日志或缓存逻辑,却不抽成 UserRepository 接口。结果新写的“代理”只能接收 *UserService,无法替换其他实现(比如 mock 或数据库版),彻底失去代理价值。
正确做法是:先写最小接口,例如:
type DataFetcher interface {
Fetch(id string) ([]byte, error)
}
- 接口越小越好,只包含核心行为,避免混入
Log、CacheKey等非契约方法 - 已有旧代码若用
*SomeStruct做参数,必须先改函数签名,否则代理无法注入 - 别用空接口
interface{}替代——它不提供任何契约,代理就失去类型安全和可替换性
代理 struct 必须持有真实对象指针并手动委托
Go 没有继承、没有方法拦截,AuthProxy 不会自动转发未实现的方法。你写了 Get(),但忘了 Delete(),调用时就会 panic 或静默失败。
立即学习“go语言免费学习笔记(深入)”;
典型结构是:
type AuthProxy struct {
svc DataFetcher
user *User
}
func (p *AuthProxy) Fetch(id string) ([]byte, error) {
if !p.user.CanAccess(id) {
return nil, errors.New("permission denied")
}
return p.svc.Fetch(id) // 必须显式调用
}
- 字段名如
svc要语义清晰,避免用inner、target这类模糊命名 - 不要嵌入
DataFetcher字段来“自动继承”——它会让未重写方法直接穿透,绕过权限控制 - 如果真实对象可能为 nil,所有方法开头都要加
if p.svc == nil { return ..., errors.New(...) }
缓存 / 延迟加载等横切逻辑要处理并发与生命周期
缓存代理不是简单加个 map[string][]byte 就完事。多个 goroutine 同时查同一个 key,会重复请求真实服务;对象初始化后没 Close,可能泄漏连接或文件句柄。
- 缓存读写必须加
sync.RWMutex,不能只读不锁——map 并发读写 panic - 延迟加载要用
sync.Once配合指针字段,例如:once sync.Once+expensive *ExpensiveResource - 若代理启了 goroutine(比如异步刷新缓存),必须暴露
Close()方法,并在退出前 cancel context、关闭 channel、释放资源 - HTTP 场景下,代理应包装
http.RoundTripper,而不是*http.Client——后者是组合型 wrapper,前者才是可插拔的传输层契约
链式代理要小心 context 和 error 传递
多个代理嵌套(比如 AuthProxy → LogProxy → CacheProxy)时,最容易漏掉 context.Context 的透传和 error 的包装。HTTP handler 中丢掉 context,超时和取消就失效;只返回 err 不 wrap,上游无法区分是权限错还是网络错。
- 每个代理方法签名应保持与原接口一致,包括是否接收
context.Context - error 建议用
fmt.Errorf("cache: %w", err)包装,保留原始堆栈 - 别在代理里做
time.Sleep或阻塞 IO——这会让整条链卡住,尤其在 HTTP server 中 - 测试时,用接口变量传入 mock 实现,比测具体 struct 更可靠
最常被忽略的一点:代理不是“加功能”,而是“控制访问时机”。只要没在调用前后插入逻辑(哪怕只是一行日志),那个 struct 就只是 wrapper,不是 proxy。


















