Table of contents
Open Table of contents
TL;DR
Go 没有继承、没有 abstract class,用隐式 interface + first-class function + channel 替代了 GoF 23 个模式中的绝大多数。真正需要手写的模式只有 Functional Options、Middleware Chain 和 Decorator 堆叠,其余要么是语言内置(Iterator = range,Singleton = sync.Once),要么是 Java 遗产在 Go 里直接不需要。
GoF 模式在 Go 中的命运
| GoF 模式 | Go 替代 | 还需要手写? |
|---|---|---|
| Strategy | func 类型 / 单方法 interface | 语言特性,不需要 |
| Command | func 值 / closure | 语言特性,不需要 |
| Observer | channel(一对一)/ fan-out(一对多) | channel 自带,fan-out 几行代码 |
| Iterator | range / Go 1.23 iter 包 | 语言内置 |
| Singleton | sync.Once + 包级变量 | 两行代码 |
| Template Method | interface + struct embedding | 组合替代继承 |
| Decorator | 接口包装(io.Reader 嵌套) | 需要理解,但实现自然 |
| Adapter | http.HandlerFunc 类型转换 | 语言特性 |
| Factory | NewXxx() 构造函数 | Go 惯例,不算模式 |
| Builder | Functional Options | Go 有更地道的方案 |
| Abstract Factory | 不需要 — Go 没有类继承 | 反模式 |
| Visitor | type switch / interface | 几乎不用 |
| Facade | 包级 API | Go 包机制天然提供 |
| Proxy | interface 包装 | 同 Decorator |
| State | func 类型 + map/switch | 函数值做状态转换 |
| Chain of Responsibility | Middleware chain | 同 Middleware |
23 个模式里约 7 个被语言直接消解,5-6 个一两行实现,剩下要么不需要要么是反模式。
Functional Options — Go 的 Builder 替代品
解决问题:构造函数参数多、大部分可选、需要合理默认值。
// Go 1.21+
type Server struct {
addr string
port int
timeout time.Duration
tls *tls.Config
}
type Option func(*Server)
func WithPort(port int) Option {
return func(s *Server) { s.port = port }
}
func WithTimeout(d time.Duration) Option {
return func(s *Server) { s.timeout = d }
}
func WithTLS(cfg *tls.Config) Option {
return func(s *Server) { s.tls = cfg }
}
func NewServer(addr string, opts ...Option) *Server {
s := &Server{
addr: addr,
port: 8080, // 合理默认值
timeout: 30 * time.Second,
}
for _, opt := range opts {
opt(s)
}
return s
}
// 调用方:只设置关心的选项
srv := NewServer("0.0.0.0",
WithPort(9090),
WithTimeout(60*time.Second),
)
为什么不用 Builder:Go 没有流式 API 的语法糖(没有返回 this),Builder 写出来是 b.SetPort(9090).SetTimeout(60) — Java 味太重。
生产中谁在用:gRPC grpc.Dial(target, ...DialOption),zap zap.New(core, ...Option),OpenTelemetry tracer.Start(ctx, name, ...SpanStartOption)。
Uber 变体:用 interface 替代 func 类型,可以给 Option 加 String() 方法用于调试日志。当选项数量多到需要调试时值得考虑。
什么时候不用 Functional Options:参数少于 3 个直接传。Options 是给 5+ 个可选参数的场景。
Middleware Chain — HTTP 的洋葱模型
解决问题:横切关注点(logging、auth、rate-limit、tracing)需要可组合、可插拔地叠加到 handler 上。
// 核心类型签名
type Middleware func(http.Handler) http.Handler
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
slog.Info("request",
"method", r.Method,
"path", r.URL.Path,
"duration", time.Since(start),
)
})
}
func Auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return // 短路,不调用 next
}
next.ServeHTTP(w, r)
})
}
// 组合
mux.Handle("/api/users", Logging(Auth(userHandler)))
洋葱模型理解:请求从外到内经过每层中间件,响应从内到外。next.ServeHTTP(w, r) 之前是”请求阶段”,之后是”响应阶段”。不调用 next 就是短路。
Chain 辅助函数:
func Chain(h http.Handler, mws ...Middleware) http.Handler {
for i := len(mws) - 1; i >= 0; i-- {
h = mws[i](h)
}
return h
}
handler := Chain(userHandler, Logging, Auth, RateLimit)
// 执行顺序:Logging → Auth → RateLimit → userHandler
http.HandlerFunc 本身就是 Adapter 模式:
// 标准库定义
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r) // 自己调用自己
}
一个带方法的函数类型,把 func(w, r) 适配为 http.Handler 接口。Go 里 Adapter 不需要 XxxAdapter 结构体,一个类型定义就够了。
Decorator 堆叠 — io.Reader/Writer 的优雅
解决问题:给数据流透明地叠加处理能力(缓冲、压缩、加密),消费方不需要知道叠了几层。
// 读取 gzip 压缩文件,三层 decorator
f, _ := os.Open("data.gz") // 底层:文件 io.Reader
defer f.Close()
br := bufio.NewReader(f) // +缓冲
gr, _ := gzip.NewReader(br) // +解压
defer gr.Close()
// gr 仍然是 io.Reader,消费方完全透明
data, _ := io.ReadAll(gr)
为什么比 Java Decorator 优雅:Go 的隐式接口 — 任何有 Read([]byte) (int, error) 方法的东西都是 io.Reader,不需要继承 AbstractDecorator。
写方向同理:
f, _ := os.Create("output.gz")
gw := gzip.NewWriter(f) // +压缩
bw := bufio.NewWriter(gw) // +缓冲
bw.WriteString("hello world")
bw.Flush() // 必须 flush 缓冲层
gw.Close() // 必须 close 压缩层(写入校验尾部)
f.Close()
自定义 Decorator:
// 计数读取字节数
type CountingReader struct {
r io.Reader
Count int64
}
func (cr *CountingReader) Read(p []byte) (int, error) {
n, err := cr.r.Read(p)
cr.Count += int64(n)
return n, err
}
cr := &CountingReader{r: resp.Body}
io.Copy(dst, cr)
fmt.Printf("read %d bytes\n", cr.Count)
Strategy 的两种形态
形态一:函数类型(单方法策略)
type HashFunc func(data []byte) []byte
func SHA256Hash(data []byte) []byte {
h := sha256.Sum256(data)
return h[:]
}
func Store(data []byte, hash HashFunc) {
checksum := hash(data)
// ...
}
Store(payload, SHA256Hash)
形态二:接口(多方法策略)
// sort.Interface — 标准库的 Strategy
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
type ByAge []Person
func (a ByAge) Len() int { return len(a) }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
sort.Sort(ByAge(people))
判断标准:一个方法 → func 类型。两个以上 → interface。不要用单方法 interface 替代 func 类型。
sync.Once — 两行 Singleton
var (
db *sql.DB
once sync.Once
)
func GetDB() *sql.DB {
once.Do(func() {
var err error
db, err = sql.Open("postgres", connStr)
if err != nil {
log.Fatal(err) // 初始化失败直接崩,不 fallback
}
})
return db
}
但 Go 社区更推荐显式注入:
func main() {
db, err := sql.Open("postgres", connStr)
if err != nil { log.Fatal(err) }
svc := NewUserService(db) // 依赖注入,不用全局单例
}
sync.Once 适合真正全局的东西(配置、logger)。业务组件用依赖注入。
Channel 模式:Go 原生的 Observer
// Fan-Out: 多个 worker 消费同一个 jobs channel
// Fan-In: 多个结果 channel 合并为一个
func fanIn(channels ...<-chan Result) <-chan Result {
var wg sync.WaitGroup
merged := make(chan Result)
for _, ch := range channels {
wg.Add(1)
go func(c <-chan Result) {
defer wg.Done()
for v := range c {
merged <- v
}
}(ch)
}
go func() { wg.Wait(); close(merged) }()
return merged
}
不需要 EventEmitter 或 Listener 接口。Go Blog “Pipelines and cancellation” 是这个模式的权威参考。
State 模式:函数值做状态转换
type StateFn func(input byte) StateFn
func stateStart(b byte) StateFn {
if b == '"' {
return stateInString
}
return stateStart
}
func stateInString(b byte) StateFn {
if b == '"' {
return stateStart
}
if b == '\\' {
return stateEscape
}
return stateInString
}
// 驱动循环
state := StateFn(stateStart)
for _, b := range input {
state = state(b)
}
标准库 text/template 的 lexer 就用这个模式。比 switch-case 状态机更清晰 — 每个状态是独立函数,转换逻辑局部化。
Go 标准库作为模式教材
| 标准库 | 体现的模式 | 说明 |
|---|---|---|
io.Reader / io.Writer | Decorator | 嵌套堆叠,每层加一个能力 |
http.HandlerFunc | Adapter | 函数类型满足接口 |
http.Handler + middleware | Chain of Responsibility | 洋葱模型 |
sort.Interface | Strategy (多方法) | Len + Less + Swap |
http.HandlerFunc | Strategy (单方法) | 函数类型即策略 |
sync.Once | Singleton | 线程安全懒初始化 |
sync.Pool | Object Pool | 复用临时对象 |
context.Context | 横切关注点传播 | 不是 GoF 模式,但比 GoF 有用 |
text/template lexer | State (函数值) | StateFn 模式 |
Pitfalls
- 接口膨胀 — 定义了 interface 但只有一个实现 → 删掉 interface,等到真需要第二个实现时再加。Go proverb: “accept interfaces, return structs”
- Functional Options 滥用 — 2-3 个参数就上 Options → 直接传参。Options 是给可选参数 5+ 的场景
- Decorator 不 Close/Flush —
gzip.Writer必须Close()写入校验尾部,bufio.Writer必须Flush()。嵌套多层时从外到内依次关闭 - Channel 当锁用 — 保护共享状态用
sync.Mutex,channel 是用来传递数据和信号的。Go proverb: “don’t communicate by sharing memory”,但也不要矫枉过正 - 照搬 Java 模式 — 在 Go 里写
AbstractFactory/AbstractStrategy→ 停下来,Go 几乎一定有更简单的方式 - 忽视
context.Context— 这不是 GoF 模式,但在 Go 生产代码里比任何 GoF 模式都重要。所有 I/O 操作、所有跨 goroutine 的取消/超时,都靠它