Skip to content

装饰器与代理模式

装饰器(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.TimeoutHandlerhttp.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.TimeoutHandlerreflect 包、io 包的各类包装器都体现了装饰器/代理思想。
  • 设计这些层时遵循「单一职责」:每层只做一件事,可以任意组合、任意顺序叠加。

下一篇讲观察者模式,看 Go 用 channel 和回调两种方式实现发布-订阅。