Go代理模式需显式构造结构体组合,通过嵌入接口、重写方法实现权限控制,用sync.Once+getter实现懒加载,HTTP代理需在middleware层处理权限与初始化。

Go 代理模式不是靠 interface{} 或 reflect 实现的
Go 没有传统 OOP 的「动态代理」机制,proxy 在 Go 里是显式构造的结构体组合,不是运行时拦截方法调用。想用 net/http/httputil.NewSingleHostReverseProxy 那种方式做 HTTP 反向代理?可以。但想给任意 struct 自动加权限校验或懒加载?必须手动封装、显式委托。
常见错误是试图用 reflect.Value.Call 包一层就叫“代理”,结果 panic:call of reflect.Value.Call on zero Value——因为没检查字段是否可导出、方法是否存在、接收者是否匹配。
- 所有被代理的方法必须定义在导出(大写开头)的接口上
- 代理 struct 必须内嵌该接口变量,或持有其实例指针
- 延迟加载逻辑只能放在 getter 方法里,不能塞进方法签名里自动触发
用嵌入+接口实现最简权限代理
权限控制的本质是「在调用前插一道检查」。Go 里最自然的做法是让代理 struct 嵌入目标接口,并重写需要鉴权的方法。
type DocumentService interface {
Get(id string) (*Document, error)
Delete(id string) error
}
type AuthProxy struct {
svc DocumentService
user *User
}
func (p *AuthProxy) Get(id string) (*Document, error) {
if !p.user.CanRead(id) {
return nil, errors.New("permission denied")
}
return p.svc.Get(id)
}
注意:AuthProxy 本身要实现 DocumentService 接口,否则无法透明替换原 service。别漏掉未重写的其他方法——要么全转发,要么明确返回错误或 panic。
立即学习“go语言免费学习笔记(深入)”;
- 嵌入
DocumentService字段可自动继承未重写的方法,但需确保它非 nil -
user字段必须在初始化时传入,不能延迟解析(比如从 context 里取),否则并发下状态不可靠 - 如果原
svc是无状态的,AuthProxy也是线程安全的;但如果user会变,就得加锁或改用函数式构造
延迟加载得靠字段 + once.Do,不是靠 proxy 自动生成
Go 没有属性访问拦截,所谓「懒加载」只能靠显式 getter 方法 + sync.Once。代理 struct 里放一个 *ExpensiveResource 字段和 sync.Once,在 getter 里初始化。
type ResourceManager struct {
once sync.Once
res *ExpensiveResource
}
func (r *ResourceManager) GetResource() *ExpensiveResource {
r.once.Do(func() {
r.res = NewExpensiveResource()
})
return r.res
}
这个 GetResource 就是你能控制加载时机的唯一入口。别指望在访问 r.res 字段时自动触发——Go 不支持属性 getter/setter。
- 字段必须是指针类型(如
*ExpensiveResource),否则once.Do修改的是副本 - 如果
NewExpensiveResource()可能失败,once.Do不处理 error,得自己包一层lazy.Load或用双检锁 - 多个字段各自用各自的
sync.Once,共用一个会导致互相阻塞
HTTP 反向代理里加权限和懒加载要改 transport 和 director
如果你用 httputil.NewSingleHostReverseProxy,它的 Director 函数只负责改请求,权限和资源初始化得在 handler 层做——比如在 proxy 前加一层 middleware。
proxy := httputil.NewSingleHostReverseProxy(target)
http.HandleFunc("/api/", func(w http.ResponseWriter, r *http.Request) {
if !checkPermission(r) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
// 这里可做懒加载:比如读配置、连 DB 连接池
lazyInitDB()
proxy.ServeHTTP(w, r)
})
注意:proxy.Transport 默认复用连接,如果懒加载的资源(如 token、证书)需要 per-request 变化,就不能塞进 transport,得在 Director 里动态设置 r.Header 或用自定义 RoundTripper。
-
Director函数里修改r.URL和r.Header是安全的;改r.Body要小心 io.EOF 和重放问题 - 不要在
Director里做耗时操作(如查 DB),它在每次请求都执行,会拖慢整个代理链 - 懒加载的资源如果带状态(如单例 client),必须保证初始化逻辑是幂等的,否则并发请求可能重复初始化










