如何使用Golang实现代理模式_Golang代理模式Proxy Pattern示例

代理对象必须实现与真实对象相同的接口,否则无法透明替换;应使用sync.Once保证延迟初始化的并发安全;代理可组合但需职责分明,避免过度嵌套;HTTP反向代理与代理模式无关。

代理对象必须实现与真实对象相同的接口

Go 没有类继承,靠接口契约约束行为。如果 RealSubjectProxy 不共用同一个接口,调用方就无法透明替换——这直接破坏代理模式的核心目标:控制访问、延迟初始化或添加横切逻辑。

常见错误是让 Proxy 只“包裹”真实对象却不实现接口,结果调用方只能写 proxy.DoSomething() 而不是面向接口编程。正确做法是定义如下的 Subject 接口:

type Subject interface {
    DoWork() string
}

然后确保 RealSubjectProxy 都实现它:

type RealSubject struct{}

func (r *RealSubject) DoWork() string {
    return "real work done"
}

type Proxy struct {
    real *RealSubject
}

func (p *Proxy) DoWork() string {
    if p.real == nil {
        p.real = &RealSubject{}
    }
    return p.real.DoWork()
}

延迟初始化时注意并发安全

代理常用于懒加载(lazy init),但多个 goroutine 同时首次调用 DoWork() 会导致重复初始化 RealSubject,甚至引发数据竞争。

不要只靠简单判空:

if p.real == nil {
    p.real = &RealSubject{} // ⚠️ 非线程安全
}

应使用 sync.Oncesync.Mutex。推荐 sync.Once,轻量且语义清晰:

type Proxy struct {
    real *RealSubject
    once sync.Once
}

func (p *Proxy) DoWork() string {
    p.once.Do(func() {
        p.real = &RealSubject{}
    })
    return p.real.DoWork()
}
  • sync.Once 保证 Do 内函数仅执行一次,无论多少 goroutine 并发触发
  • 避免在 DoWork 中加锁整个方法体——那会严重拖慢吞吐
  • 若初始化逻辑需传参或返回错误,sync.Once 不够用,得改用带锁的 lazy-init 模式

代理可组合多种行为,但别过度嵌套

一个 Proxy 可同时做日志、权限校验、缓存、重试等事,但每层职责要分明。比如:

type LoggingProxy struct {
    next Subject
}
func (l *LoggingProxy) DoWork() string {
    log.Println("before")
    res := l.next.DoWork()
    log.Println("after")
    return res
}

type AuthProxy struct {
    next Subject
    user string
}
func (a *AuthProxy) DoWork() string {
    if a.user != "admin" {
        return "access denied"
    }
    return a.next.DoWork()
}

组合时顺序很重要:AuthProxy{next: LoggingProxy{next: real}} 表示先鉴权再打日志;反过来就可能在

未授权时也输出了 “before”。

  • 组合建议用构造函数封装,避免手动 new 多层嵌套出错
  • 不要让一个代理既做缓存又做限流又做熔断——拆成独立代理更易测、易复用
  • 每个代理应只依赖 Subject 接口,不感知下一层具体类型

HTTP 反向代理不是 Go 标准库的 net/http/httputil.NewSingleHostReverseProxy

很多人搜“Golang 代理模式”却误入 HTTP 反向代理场景。这是两类东西:net/http/httputil 是网络层代理,而 Proxy Pattern 是面向对象设计模式,解决的是程序内对象访问控制问题。

如果你实际想做的是转发 HTTP 请求(比如网关),那应该用:

import "net/http/httputil"

proxy := httputil.NewSingleHostReverseProxy(&url.URL{
    Scheme: "http",
    Host:   "localhost:8081",
})
http.ListenAndServe(":8080", proxy)

但这和你定义 Subject 接口、写 Proxy 结构体完全无关。混淆这两者会导致架构分层错乱——业务逻辑层不该混入 HTTP transport 细节。

真正难的是:当你要代理一个已有结构体(比如第三方库的 Client)且它没提供接口时,你得自己抽象出接口,再写适配器 + 代理。这个环节最容易被跳过,结果就是代理变成硬编码耦合,失去扩展性。