跳转到正文
zeno's blog
返回

Go 基础:interface 与 first-class function 如何消解 GoF 模式

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 替代还需要手写?
Strategyfunc 类型 / 单方法 interface语言特性,不需要
Commandfunc 值 / closure语言特性,不需要
Observerchannel(一对一)/ fan-out(一对多)channel 自带,fan-out 几行代码
Iteratorrange / Go 1.23 iter语言内置
Singletonsync.Once + 包级变量两行代码
Template Methodinterface + struct embedding组合替代继承
Decorator接口包装(io.Reader 嵌套)需要理解,但实现自然
Adapterhttp.HandlerFunc 类型转换语言特性
FactoryNewXxx() 构造函数Go 惯例,不算模式
BuilderFunctional OptionsGo 有更地道的方案
Abstract Factory不需要 — Go 没有类继承反模式
Visitortype switch / interface几乎不用
Facade包级 APIGo 包机制天然提供
Proxyinterface 包装同 Decorator
Statefunc 类型 + map/switch函数值做状态转换
Chain of ResponsibilityMiddleware 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
}

不需要 EventEmitterListener 接口。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.WriterDecorator嵌套堆叠,每层加一个能力
http.HandlerFuncAdapter函数类型满足接口
http.Handler + middlewareChain of Responsibility洋葱模型
sort.InterfaceStrategy (多方法)Len + Less + Swap
http.HandlerFuncStrategy (单方法)函数类型即策略
sync.OnceSingleton线程安全懒初始化
sync.PoolObject Pool复用临时对象
context.Context横切关注点传播不是 GoF 模式,但比 GoF 有用
text/template lexerState (函数值)StateFn 模式

Pitfalls

  1. 接口膨胀 — 定义了 interface 但只有一个实现 → 删掉 interface,等到真需要第二个实现时再加。Go proverb: “accept interfaces, return structs”
  2. Functional Options 滥用 — 2-3 个参数就上 Options → 直接传参。Options 是给可选参数 5+ 的场景
  3. Decorator 不 Close/Flushgzip.Writer 必须 Close() 写入校验尾部,bufio.Writer 必须 Flush()。嵌套多层时从外到内依次关闭
  4. Channel 当锁用 — 保护共享状态用 sync.Mutex,channel 是用来传递数据和信号的。Go proverb: “don’t communicate by sharing memory”,但也不要矫枉过正
  5. 照搬 Java 模式 — 在 Go 里写 AbstractFactory / AbstractStrategy → 停下来,Go 几乎一定有更简单的方式
  6. 忽视 context.Context — 这不是 GoF 模式,但在 Go 生产代码里比任何 GoF 模式都重要。所有 I/O 操作、所有跨 goroutine 的取消/超时,都靠它

分享这篇文章:

上一篇
现代 C++(三):Concepts、Ranges、Coroutines、Modules 如何重定义 C++
下一篇
现代 C++(二):if constexpr、optional 与 variant 如何降低认知负担