Appearance
装饰器与代理模式
装饰器(Decorator)和代理(Proxy)在结构上几乎一样——都是「包装一个对象,控制对它的访问」。它们的区别在意图:装饰器为对象动态添加职责(增强功能),代理控制对对象的访问(管理访问)。本章我们看 Go 如何用「嵌入接口」优雅实现这两个模式,并说明它们与 Go 中间件的关系。
一、装饰器模式
1. 意图
装饰器模式:动态地为一个对象添加额外的职责,而不改变其接口。装饰器比子类化(继承)更灵活——可以在运行时组合多种装饰。
经典场景:
- 给函数调用加日志、加缓存、加性能计数。
- 给 HTTP Handler 加压缩、加鉴权、加限流。
- 给数据流加加密、加压缩(如
gzip.Reader包装io.Reader)。
2. Go 实现:包装 + 嵌入接口
Go 装饰器的标准写法:定义一个结构体,嵌入目标接口作为字段,实现该接口时把调用转发给被嵌入的对象,并在转发前后做附加逻辑。
go
package main
import "fmt"
// === 目标接口 ===
type Greeter interface {
Greet(name string) string
}
// === 被装饰的具体对象 ===
type EnglishGreeter struct{}
func (EnglishGreeter) Greet(name string) string {
return "Hello, " + name
}
// === 装饰器:嵌入 Greeter 接口 ===
type LogDecorator struct {
inner Greeter
}
func NewLogDecorator(g Greeter) *LogDecorator {
return &LogDecorator{inner: g}
}
func (d *LogDecorator) Greet(name string) string {
fmt.Printf("[LOG] 调用 Greet(%q)\n", name)
result := d.inner.Greet(name) // 转发给被包装对象
fmt.Printf("[LOG] 返回 %q\n", result)
return result
}
// 装饰器可以叠加
type UpperCaseDecorator struct {
inner Greeter
}
func (d *UpperCaseDecorator) Greet(name string) string {
return d.inner.Greet(name) + "!"
}
func main() {
var g Greeter = EnglishGreeter{}
// 先加日志,再加大写
g = NewLogDecorator(g)
g = &UpperCaseDecorator{inner: g}
fmt.Println(g.Greet("Alice"))
}注意装饰器叠加:g = &UpperCaseDecorator{inner: NewLogDecorator(EnglishGreeter{})}。装饰器像洋葱一样层层包裹,每层在转发前后做自己该做的事。这种「洋葱模型」正是 Go HTTP 中间件的核心思想。
3. Go 中装饰器与中间件的关系
Go 社区里,「中间件(Middleware)」几乎就是「装饰器」的别名。一个 HTTP 中间件签名是:
go
func(http.Handler) http.Handler它接收一个 Handler,返回一个新的 Handler,新 Handler 在调用原 Handler 前后做额外的事——这就是装饰器。看下面的例子:
go
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"time"
)
// === 基础 Handler ===
func helloHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("hello"))
}
// === 中间件 1:日志 ===
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
fmt.Printf("[LOG] %s %s 耗时 %v\n", r.Method, r.URL.Path, time.Since(start))
})
}
// === 中间件 2:鉴权 ===
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") == "" {
http.Error(w, "未授权", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
// === 中间件 3:响应头 ===
func headerMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-App", "demo")
next.ServeHTTP(w, r)
})
}
// 链式组合多个中间件
func chain(h http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
h = middlewares[i](h)
}
return h
}
func main() {
// 组装:日志 -> 鉴权 -> 响应头 -> hello
handler := chain(
http.HandlerFunc(helloHandler),
loggingMiddleware,
authMiddleware,
headerMiddleware,
)
// 测试 1:无 token
req := httptest.NewRequest("GET", "/hello", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
fmt.Printf("无 token 状态码: %d\n", w.Code)
// 测试 2:有 token
req2 := httptest.NewRequest("GET", "/hello", nil)
req2.Header.Set("Authorization", "Bearer xxx")
w2 := httptest.NewRecorder()
handler.ServeHTTP(w2, req2)
fmt.Printf("有 token 状态码: %d, body: %q, X-App: %q\n",
w2.Code, w2.Body.String(), w2.Header().Get("X-App"))
}chain 函数把多个中间件按顺序套在最终 Handler 外面,这就是 Gin、Echo、Chi 等框架的中间件机制本质(只是它们封装得更精美)。理解了装饰器,就理解了 Web 中间件。
4. 实战示例:日志装饰器、缓存装饰器、HTTP Handler 装饰
下面把装饰器用在业务对象上(不限于 HTTP Handler),演示日志 + 缓存两层装饰:
go
package main
import (
"fmt"
"time"
)
// === 业务接口 ===
type UserService interface {
GetUser(id string) (string, error)
}
// === 基础实现 ===
type userService struct{}
func (userService) GetUser(id string) (string, error) {
time.Sleep(50 * time.Millisecond) // 模拟查库
return "user-" + id, nil
}
// === 日志装饰器 ===
type logDecorator struct {
inner UserService
}
func (d *logDecorator) GetUser(id string) (string, error) {
start := time.Now()
result, err := d.inner.GetUser(id)
fmt.Printf("[LOG] GetUser(%s) 耗时 %v err=%v\n", id, time.Since(start), err)
return result, err
}
// === 缓存装饰器 ===
type cacheDecorator struct {
inner UserService
cache map[string]string
}
func newCacheDecorator(inner UserService) *cacheDecorator {
return &cacheDecorator{inner: inner, cache: map[string]string{}}
}
func (d *cacheDecorator) GetUser(id string) (string, error) {
if v, ok := d.cache[id]; ok {
fmt.Printf("[CACHE] 命中 %s\n", id)
return v, nil
}
result, err := d.inner.GetUser(id)
if err == nil {
d.cache[id] = result
}
return result, err
}
func main() {
// 组装:缓存包裹日志,日志包裹真实服务
var svc UserService = userService{}
svc = &logDecorator{inner: svc}
svc = newCacheDecorator(svc)
// 第一次:未命中缓存,走真实服务
r1, _ := svc.GetUser("1")
fmt.Println("结果1:", r1)
// 第二次:命中缓存
r2, _ := svc.GetUser("1")
fmt.Println("结果2:", r2)
}这个例子展示了装饰器的核心价值:单一职责。userService 只管业务,logDecorator 只管日志,cacheDecorator 只管缓存。三者可以任意组合、任意顺序叠加,互不耦合。
二、代理模式
1. 意图
代理模式:为其他对象提供一种代理以控制对这个对象的访问。代理和被代理对象实现相同的接口,客户端无感知。
代理类型通常分三种:
- 远程代理(Remote Proxy):为代表远程对象(如 RPC 客户端)的本地对象。
- 虚拟代理(Virtual Proxy):延迟创建昂贵的对象(懒加载)。
- 保护代理(Protection Proxy):在访问前做权限检查。
2. Go 实现:与装饰器结构相似,意图不同
代理的代码结构和装饰器几乎一模一样——都是嵌入目标接口 + 转发调用。区别在意图:装饰器为「增强功能」加层,代理为「控制访问」加层。
go
package main
import (
"fmt"
"sync"
)
// === 目标接口 ===
type Image interface {
Display()
}
// === 真实对象:加载代价高 ===
type RealImage struct {
filename string
}
func NewRealImage(filename string) *RealImage {
fmt.Printf("加载图片 %s(耗时操作)...\n", filename)
return &RealImage{filename: filename}
}
func (r *RealImage) Display() {
fmt.Printf("显示图片 %s\n", r.filename)
}
// === 虚拟代理:延迟创建 RealImage ===
type ProxyImage struct {
filename string
real *RealImage
once sync.Once
}
func NewProxyImage(filename string) *ProxyImage {
return &ProxyImage{filename: filename}
}
func (p *ProxyImage) Display() {
// 第一次访问才创建真实对象(懒加载)
p.once.Do(func() {
p.real = NewRealImage(p.filename)
})
p.real.Display()
}
func main() {
fmt.Println("=== 创建代理(不加载)===")
img := NewProxyImage("big-photo.jpg")
fmt.Println("=== 第一次 Display(触发加载)===")
img.Display()
fmt.Println("=== 第二次 Display(复用)===")
img.Display()
}输出可以看到:创建代理时不会触发加载,第一次 Display 才真正加载,第二次直接复用。这就是虚拟代理——把昂贵的创建推迟到真正需要时。
3. 保护代理示例
go
package main
import "fmt"
type DataStore interface {
Read(key string) string
Write(key string, value string)
}
type memoryStore struct {
data map[string]string
}
func (m *memoryStore) Read(key string) string {
return m.data[key]
}
func (m *memoryStore) Write(key string, value string) {
m.data[key] = value
}
// 保护代理:根据角色控制访问
type ProtectedStore struct {
inner DataStore
role string
}
func NewProtectedStore(inner DataStore, role string) *ProtectedStore {
return &ProtectedStore{inner: inner, role: role}
}
func (p *ProtectedStore) Read(key string) string {
if p.role != "admin" && p.role != "user" {
fmt.Println("拒绝:无读取权限")
return ""
}
return p.inner.Read(key)
}
func (p *ProtectedStore) Write(key, value string) {
if p.role != "admin" {
fmt.Printf("拒绝:%s 无写入权限\n", p.role)
return
}
p.inner.Write(key, value)
}
func main() {
store := &memoryStore{data: map[string]string{}}
// admin 视角
admin := NewProtectedStore(store, "admin")
admin.Write("k1", "v1")
fmt.Println("admin 读:", admin.Read("k1"))
// user 视角
user := NewProtectedStore(store, "user")
fmt.Println("user 读:", user.Read("k1"))
user.Write("k2", "v2") // 被拒
// guest 视角
guest := NewProtectedStore(store, "guest")
guest.Read("k1") // 被拒
}保护代理在 RPC、权限系统、缓存层非常常见。比如一个微服务客户端可以包一层保护代理,校验请求是否合法、是否携带 token,再转发给真实的服务。
4. 远程代理示例
go
package main
import "fmt"
// Calculator 是远程服务的接口
type Calculator interface {
Add(a, b int) int
}
// RemoteCalculator 代理:模拟一个 RPC 客户端
type RemoteCalculator struct {
serverAddr string
}
func NewRemoteCalculator(addr string) *RemoteCalculator {
return &RemoteCalculator{serverAddr: addr}
}
func (r *RemoteCalculator) Add(a, b int) int {
// 实际场景:序列化参数,发起网络请求,反序列化结果
fmt.Printf("[RPC] 调用 %s.Add(%d, %d)\n", r.serverAddr, a, b)
return a + b // 模拟
}
func main() {
// 客户端用起来就像本地对象,实际是远程调用
calc := NewRemoteCalculator("calc-service:50051")
fmt.Println("结果:", calc.Add(3, 5))
}gRPC 生成的 Go 客户端 stub 就是典型的远程代理:你调用本地方法,它把请求转发到远端服务器。这种「透明远程调用」让分布式系统写起来像本地程序,是微服务架构的基础。
三、装饰器 vs 代理:结构相同,意图不同
两者代码结构几乎一致(嵌入接口 + 转发),但意图有明确区分:
| 维度 | 装饰器 | 代理 |
|---|---|---|
| 意图 | 增强(加职责) | 控制(管访问) |
| 谁创建被包装对象 | 调用方传入 | 代理内部创建/管理 |
| 生命周期 | 通常无状态,按需创建 | 代理往往管理对象生命周期 |
| 典型场景 | 日志、缓存、压缩、限流 | 懒加载、权限、远程调用 |
| 数量 | 可任意叠加多层 | 通常是单层(特殊场景才嵌套) |
| 客户端是否感知 | 不感知 | 不感知 |
一个简单的判断标准:
- 如果包装层是为了「让对象做更多事」→ 装饰器。
- 如果包装层是为了「不让客户端直接接触对象」→ 代理。
实际项目中两者经常混用,比如一个「带缓存的代理」既控制访问又增强功能,不必死抠术语。
四、标准库中的应用
1. reflect 包的代理模式
reflect 包提供的反射能力本质上是一种「代理」:你通过 reflect.Value 操作底层值,而不是直接操作它。reflect.Value 控制了对原值的访问(可以读、可以设、可以调用方法),相当于一个带元信息的代理。这属于「智能引用代理」。
2. net/http 的中间件(装饰器)
前面已经讲过,http.Handler 中间件就是装饰器。Go 标准库自带的 http.TimeoutHandler、http.FileServer 配合中间件使用,都是装饰器的体现:
go
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"time"
)
func slowHandler(w http.ResponseWriter, r *http.Request) {
time.Sleep(200 * time.Millisecond)
w.Write([]byte("done"))
}
func main() {
// 用 http.TimeoutHandler 包装,限制 50ms
h := http.TimeoutHandler(http.HandlerFunc(slowHandler), 50*time.Millisecond, "超时了")
req := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
h.ServeHTTP(w, req)
fmt.Printf("状态码: %d, body: %q\n", w.Code, w.Body.String())
}http.TimeoutHandler 接收一个 Handler,返回一个新的 Handler,新 Handler 在超时后返回 503——典型的装饰器/代理行为。
3. sync.Pool(虚拟代理思想)
sync.Pool 管理一组可复用对象,Get 时按需创建或复用,体现「延迟创建 + 复用」的代理思想。它本身不是严格的代理模式,但思路相通。
五、综合实战:带缓存、日志、限流的服务代理
下面这个例子把装饰器和代理混用,构造一个完整的服务访问层:
go
package main
import (
"fmt"
"sync"
"time"
)
// 业务接口
type StockService interface {
GetPrice(symbol string) (float64, error)
}
// 真实实现:调用昂贵
type realStockService struct{}
func (realStockService) GetPrice(symbol string) (float64, error) {
time.Sleep(100 * time.Millisecond) // 模拟网络耗时
return 100.0 + float64(len(symbol)), nil
}
// 缓存装饰器
type cachedService struct {
inner StockService
mu sync.Mutex
cache map[string]float64
}
func newCachedService(inner StockService) *cachedService {
return &cachedService{inner: inner, cache: map[string]float64{}}
}
func (c *cachedService) GetPrice(symbol string) (float64, error) {
c.mu.Lock()
if v, ok := c.cache[symbol]; ok {
c.mu.Unlock()
return v, nil
}
c.mu.Unlock()
v, err := c.inner.GetPrice(symbol)
if err != nil {
return 0, err
}
c.mu.Lock()
c.cache[symbol] = v
c.mu.Unlock()
return v, nil
}
// 日志装饰器
type loggedService struct {
inner StockService
}
func (l *loggedService) GetPrice(symbol string) (float64, error) {
start := time.Now()
v, err := l.inner.GetPrice(symbol)
fmt.Printf("[LOG] GetPrice(%s) = %.2f, 耗时 %v\n", symbol, v, time.Since(start))
return v, err
}
// 限流代理(保护)
type rateLimitedService struct {
inner StockService
tokens chan struct{}
}
func newRateLimitedService(inner StockService, rate int) *rateLimitedService {
rl := &rateLimitedService{
inner: inner,
tokens: make(chan struct{}, rate),
}
// 每秒填充 rate 个令牌
go func() {
ticker := time.NewTicker(time.Second / time.Duration(rate))
for range ticker.C {
select {
case rl.tokens <- struct{}{}:
default:
}
}
}()
// 预填充一些令牌
for i := 0; i < rate; i++ {
rl.tokens <- struct{}{}
}
return rl
}
func (r *rateLimitedService) GetPrice(symbol string) (float64, error) {
<-r.tokens // 取令牌,否则阻塞
return r.inner.GetPrice(symbol)
}
func main() {
// 组装:日志 -> 缓存 -> 限流 -> 真实服务
var svc StockService = realStockService{}
svc = newRateLimitedService(svc, 10)
svc = newCachedService(svc)
svc = &loggedService{inner: svc}
for _, s := range []string{"AAPL", "GOOG", "AAPL", "TSLA"} {
v, _ := svc.GetPrice(s)
fmt.Printf(" -> %s: %.2f\n", s, v)
}
}这个组合体现了工程实战中的常见分层:
- 最底层
realStockService调用真实数据源。 rateLimitedService是保护代理,防止下游被打爆。cachedService是缓存装饰器,减少重复调用。loggedService是日志装饰器,记录调用情况。- 业务代码拿到的是一个透明的
StockService,不关心背后包了多少层。
六、小结
- 装饰器动态地为对象添加职责,不改变其接口;Go 用「嵌入接口 + 转发调用」实现,比继承更灵活。
- 代理控制对对象的访问,分虚拟代理(懒加载)、保护代理(权限)、远程代理(RPC)等。
- Go 的 HTTP 中间件就是装饰器:
func(http.Handler) http.Handler。 - 装饰器和代理结构相同,区别在意图——装饰器为增强,代理为控制。
- 实战中两者常混用:一个服务访问层可以同时有日志(装饰)、缓存(装饰)、限流(代理)、懒加载(虚拟代理)。
- 标准库中
http.TimeoutHandler、reflect包、io包的各类包装器都体现了装饰器/代理思想。 - 设计这些层时遵循「单一职责」:每层只做一件事,可以任意组合、任意顺序叠加。
下一篇讲观察者模式,看 Go 用 channel 和回调两种方式实现发布-订阅。