Go代理模式需手动实现接口全部方法,嵌入不能自动透传;须显式转发调用、空指针防护、sync.Once延迟初始化、函数式权限校验及职责清晰的字段命名。

Go 里没有语言级的 Proxy 类型,所谓“实现代理模式”,就是手动定义接口、写结构体、显式转发调用——没捷径,但错一点就编译不过或运行 panic。
代理必须显式实现全部接口方法,不能靠嵌入自动透传
很多人想省事,在代理结构体里嵌入 DataService 字段,以为这样就能“继承”方法。不行。Go 的嵌入只提供字段访问和方法提升(promoted methods),但前提是:被嵌入类型本身已实现该接口;且代理类型没有同名方法覆盖它。一旦你加了权限逻辑,就必须自己写 Get、Save、Delete 等全部方法体。
常见错误包括:
- 漏写某个方法,导致代理类型无法满足接口,编译报错
missing method Delete - 参数名或返回值顺序不一致(比如把
func(id int) (string, error)写成func(ID int) (error, string)),编译器直接拒绝 - 用值接收者实现接口,但代理字段是
*RealService,调用时隐式解引用失败
正确做法是:每个方法里都明确写 p.real.Get(id) 或加判断后决定是否调用。别幻想反射或泛型能帮你绕过接口签名检查——Go 不允许。
立即学习“go语言免费学习笔记(深入)”;
真实对象为 nil 时,所有方法开头必须做空指针防护
延迟加载的核心是让 real 字段初始化为 nil,首次调用再构造。但 Go 不会自动帮你判空,一旦 p.real == nil 还直接调 p.real.Fetch(),就会 panic: invalid memory address or nil pointer dereference。
安全写法是:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 每个代理方法第一行加
if p.real == nil { p.initReal() } -
initReal必须用sync.Once包裹,否则并发调用会重复初始化(比如多次 dial 数据库) - 如果初始化可能失败(如配置缺失、网络不通),
initReal应返回 error,并缓存失败状态,避免每次重试
别在方法里裸写 p.real = &RealService{...} ——这不是线程安全的。
权限控制别硬编码在方法里,要抽成可替换函数
把 if !p.user.CanAccess(id) 直接塞进每个方法,看着短,实际是技术债。改个权限策略就得改七八个地方;测单个方法还得 mock 用户对象;日志、审计、限流等逻辑全挤在一起,谁也看不懂。
推荐结构:
- 定义鉴权函数类型:
type AccessChecker func(ctx context.Context, method string, args interface{}) error - 代理结构体持有一个
checker AccessChecker字段 - 每个方法开头统一调
p.checker(ctx, "Fetch", id),失败立刻 return - 真实对象方法本身的
error要原样透传,别和权限错误混为一谈(HTTP 层需区分403 Forbidden和500 Internal Error)
这样换一种鉴权方式(比如从 Redis 查、走 gRPC 调鉴权服务),只换一个函数,不碰代理结构体。
字段命名要语义清晰,别用 inner/target/real 这类模糊名
inner、target、real 这些名字看不出用途,读代码时得翻好几层才能确认它到底干啥。更糟的是,当你要加第二个横切逻辑(比如缓存 + 权限),字段名就彻底失效了。
应该按职责命名:
- 做权限代理 → 字段叫
svc DataService(强调它是被控的服务) - 做缓存代理 → 字段叫
backend DataService(强调它是后端源) - 做日志代理 → 字段叫
next DataService(强调它是链式调用的下一环)
名字即契约。看到 svc 就知道这是核心业务逻辑入口;看到 backend 就明白它可能被缓存绕过;看到 next 就准备加中间件链。这点小事,影响后续所有人理解成本。

















