大家好,我是十三!欢迎来到十三Tech。

第一次用 gRPC 的时候我特别震撼——客户端代码就是 client.GetUser(ctx, &pb.GetUserRequest{ID: 1}),看起来完全是本地函数调用。但底层其实经历了序列化、TCP 连接、HTTP/2 流、对端反射、反序列化。客户端完全无感,这种"看起来在用本地对象,实际被一层代理拦截"的设计就是代理模式

代理在工程里比想象中常见——RPC 客户端、数据库连接池、缓存层、权限校验、懒加载……它们的共同点都是客户端以为在用真实对象,实际经过了一层代理。这一篇就聊聊代理的标志特征是"透明拦截",以及它跟装饰器的微妙差别。

代理的现实类比:经纪人代理明星接通告

一、代理在解决什么——不是"加一层"

教科书定义:为其他对象提供代理以控制对这个对象的访问。这话模糊到可以套在任何 wrapper 上——适配器、装饰器、门面都可以叫"代理"。真正的代理有一个标志特征——透明性

透明性指的是客户端完全不知道自己用的是代理还是真实对象。装饰器是调用方主动包装的,调用方知道自己在叠加功能;代理是装配阶段就插好的,调用方拿到的就是代理对象,从签名到行为都跟真实对象一模一样。

代理的几种典型变体——虚拟代理(懒加载)、保护代理(权限校验)、缓存代理(结果缓存)、远程代理(RPC stub)——它们的共同点都是"对调用方隐藏内部细节"

判断口诀:调用方无感 → 代理;调用方主动包装 → 装饰器

二、Go 里代理怎么写——同接口持有真实对象

代理实现和真实对象相同的接口,内部持有真实对象的引用。调用方拿到的是代理,代理决定何时、如何、是否调用真实对象。

代理 UML 结构:Proxy 与 RealSubject 同接口

type UserService interface { GetUser(id int) (*User, error) }

type cacheProxy struct {
    real  UserService
    cache sync.Map
}

func (p *cacheProxy) GetUser(id int) (*User, error) {
    if v, ok := p.cache.Load(id); ok { return v.(*User), nil }
    u, err := p.real.GetUser(id)
    if err != nil { return nil, err }
    p.cache.Store(id, u)
    return u, nil
}

cacheProxy——它实现了 UserService 接口,内部持有真实的 UserService客户端代码完全一样svc.GetUser(1)),但行为已经被代理改变了——第一次查 DB,第二次查缓存。

代理的精髓在装配阶段决定。客户端拿到的是 NewCacheProxy(NewUserService(db)),但客户端只看到 UserService 接口,不知道也不需要知道中间套了几层代理。这种"装配时透明叠加、调用时无感使用"是代理区别于装饰器的核心

代理调用流程:客户端 → Proxy(拦截)→ RealSubject

三、四种代理的真实场景

代理家族有四个常见变体,每个解决不同问题。

虚拟代理——真实对象创建昂贵,按需创建。比如 lazyDB 包装一个数据库连接,第一次 Query 时才真正建连。这种代理在 ORM 里特别常见。

保护代理——调用前做权限校验。比如 authProxy 检查 token 是否有效,无效直接返回 401,有效才转发到真实对象。网关层就是这个模式。

缓存代理——结果可复用,缓存避免重复计算。cacheProxy 缓存 GetUser 结果,读多写少场景下能省 90%+ 的 DB 查询。

远程代理——本地调用透明转发到远程。RPC client 就是教科书级的远程代理——client.GetUser() 看起来是本地调用,实际走了网络。gRPC、Thrift、Dubbo 全是这种设计。

代理适用决策:四种代理类型的选择

代理的反模式主要有两个。

反模式一:代理里塞业务逻辑。代理只做控制——缓存、权限、懒加载、转发。如果代理里写了"判断用户等级""计算折扣金额",它已经退化成了业务层。代理要保持薄,业务逻辑在 RealSubject。

反模式二:代理的接口跟真实对象不一致。如果代理加了新方法(比如 cacheProxy.RefreshCache()),客户端就感知到了代理的存在——透明性破坏。代理必须严格实现真实对象的接口,不多一个方法、不少一个方法

Go 标准库里代理最典型的地方是 database/sql.DB——*sql.DB 不是单个数据库连接,它是一个连接池代理。你调 db.Query,背后是 DB 帮你从连接池借一个连接、执行查询、还回去。你以为是单连接操作,实际是池化管理。这种"客户端看到 DB,背后是连接池"的设计,就是代理思想的精髓。

回头看 gRPC client——值得用代理吗?值得,远程调用、序列化、连接管理这些复杂度必须对调用方隐藏,远程代理是唯一答案。但如果只是给某个对象加日志,用装饰器更合适——装饰器让调用方知道自己在叠加,代理让调用方完全无感,意图不同选择不同。

代理真正教会我的,不是"控制访问",而是区分"调用方该知道的"和"调用方不该知道的"——调用方该知道的(叠加行为)用装饰器,调用方不该知道的(远程/缓存/懒加载)用代理。这个区分比模式本身重要得多。

下一篇讲门面模式,它和代理一样"加一层",但意图是简化复杂子系统的访问。


关于十三Tech 资深服务端研发,AI 实践者,专注分享真实可落地的技术经验。 相信 AI 是程序员的最佳搭档。

联系方式:569893882@qq.com GitHub:@TriTechAI