Go 设计模式实战:从接口组合到选项模式的 Go 风格实现

小小卒子2026-10-20后端⏱ 8min

Go 设计模式实战:从接口组合到选项模式的 Go 风格实现

设计模式在 Go 里常常被误解为"Java 那套搬过来"——其实 Go 的哲学是"组合优于继承,接口要小"。本文用 Go 的惯用写法重新演绎工厂、策略、单例、观察者、装饰器等八个高频模式,让你看到"Go 风格"的答案长什么样,以及哪些模式在 Go 里干脆不需要。

一、Go 与经典设计模式的关系

Go 没有类继承,只有结构体嵌入和接口。这对设计模式的影响是革命性的:

| 经典模式 | Java 惯用实现 | Go 惯用实现 | | --- | --- | --- | | 工厂模式 | 抽象工厂类 + 子类 | 函数返回接口,构造逻辑放函数里 | | 策略模式 | 策略接口 + 策略类 | 接口 + 函数变量(一等公民) | | 观察者 | 观察者接口 + 订阅管理 | channel 或回调函数 | | 装饰器 | 装饰器类层层包装 | 函数包装函数(中间件) | | 单例 | 私有构造 + 静态实例 | sync.Once 或包级变量 |

核心思想:Go 里接口定义的应该是"行为"而不是"类型层次",模式的目的(解耦、复用、扩展)用语言特性实现得更轻。

二、工厂模式:函数式工厂

Go 的工厂就是"一个返回接口的函数",不需要类层次:

type Parser interface {
    Parse(data []byte) ([]Record, error)
}

func NewParser(format string) (Parser, error) {
    switch format {
    case "json":
        return jsonParser{}, nil
    case "csv":
        return csvParser{}, nil
    default:
        return nil, fmt.Errorf("unsupported format: %s", format)
    }
}

变体工厂方法注册表(注册表模式)适合插件化,避免 switch 膨胀:

var registry = map[string]func() Parser{}

func Register(name string, factory func() Parser) {
    registry[name] = factory
}

func NewParser(format string) (Parser, error) {
    f, ok := registry[format]
    if !ok {
        return nil, fmt.Errorf("unsupported: %s", format)
    }
    return f(), nil
}

新增一种格式只需要 Register,主代码零改动——这就是开闭原则的 Go 版。

三、策略模式:接口即策略

策略模式在 Go 里最简单——定义一个接口,函数接收接口参数:

type Encoder interface {
    Encode(v any) ([]byte, error)
}

type JSONEncoder struct{}
func (JSONEncoder) Encode(v any) ([]byte, error) { return json.Marshal(v) }

type MsgPackEncoder struct{}
func (MsgPackEncoder) Encode(v any) ([]byte, error) { /* ... */ }

func SendReport(e Encoder, data any) ([]byte, error) {
    return e.Encode(data)
}

更进一步的 Go 特色:函数本身就是策略。对只有一个方法的接口,直接传函数:

type Hook func(ctx context.Context) error

func RunWithRetry(h Hook, retries int) error {
    var err error
    for i := 0; i < retries; i++ {
        if err = h(ctx); err == nil {
            return nil
        }
        time.Sleep(time.Second << i)
    }
    return err
}

Go 的"接口只要一个方法"(如 io.Reader、http.Handler、error)就是这个思想的极致,配合函数值可以写出极富表现力的组合逻辑。

四、单例模式:sync.Once 是标准答案

Go 里单例最简单可靠的实现是 sync.Once——并发安全、只执行一次、开销极小:

var (
    instance *Config
    once     sync.Once
)

func GetConfig() *Config {
    once.Do(func() {
        instance = loadFromFile("config.yaml")
    })
    return instance
}

对比 init() 和包级变量:sync.Once 支持延迟初始化(第一次使用时才加载),且不受程序启动顺序影响。注意单例要克制使用——它隐藏依赖、难测试,Go 社区更推崇"显式传依赖"。

五、选项模式:函数式参数

这是 Go 独有的、最值得掌握的惯用法。当构造函数参数超过 3 个时,用"选项函数"替代啰嗦的参数列表:

type Server struct {
    port    int
    timeout time.Duration
    tls     bool
}

type Option func(*Server)

func WithPort(p int) Option       { return func(s *Server) { s.port = p } }
func WithTimeout(d time.Duration) Option { return func(s *Server) { s.timeout = d } }
func WithTLS(b bool) Option       { return func(s *Server) { s.tls = b } }

func NewServer(opts ...Option) *Server {
    s := &Server{port: 8080, timeout: 30 * time.Second} // 默认值
    for _, opt := range opts {
        opt(s)
    }
    return s
}

// 使用:只需要配置关心的项
srv := NewServer(WithPort(9090), WithTLS(true))

优点:

  1. 默认值 + 可选参数并存,调用方不必记参数顺序;
  2. 向后兼容:新增配置项只需加一个 Option,老调用代码不破坏;
  3. 可扩展:Option 本身可以是复杂逻辑(校验、依赖注入)。

Google 的 gRPC、GORM 等重量级库全部采用这种模式,是 Go 工程界的"事实标准"。

六、观察者模式:channel 与回调

Go 的观察者有两种惯用写法:

1. 回调式:

type EventBus struct {
    mu   sync.RWMutex
    subs map[string][]func(Event)
}

func (b *EventBus) Subscribe(topic string, fn func(Event)) {
    b.mu.Lock()
    defer b.mu.Unlock()
    b.subs[topic] = append(b.subs[topic], fn)
}

func (b *EventBus) Publish(topic string, e Event) {
    b.mu.RLock()
    fns := append([]func(Event){}, b.subs[topic]...)
    b.mu.RUnlock()
    for _, fn := range fns {
        fn(e)
    }
}

注意 Publish 里拷贝订阅者切片再遍历,避免在遍历时加锁导致长时间阻塞写者。

2. channel 式:用带缓冲 channel 做异步事件流,配合 goroutine 消费,天然支持背压和取消(context)。

七、装饰器模式:中间件链

Go 的 HTTP 中间件就是装饰器——函数包装函数:

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", "path", r.URL.Path, "dur", time.Since(start))
    })
}

func Recovery(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if p := recover(); p != nil {
                slog.Error("panic", "err", p)
                http.Error(w, "internal error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

// 链式组合
handler := Recovery(Logging(mux))

每个中间件只做一件事,任意组合复用——这就是装饰器模式在 Go 里的自然形态。

八、适配器与组合

适配器模式在 Go 里就是"实现目标接口":

type Writer interface{ Write(p []byte) (int, error) }

type JSONWriter struct{ w io.Writer }
func (j JSONWriter) Write(p []byte) (int, error) {
    return j.w.Write(append([]byte{}, p...)) // 加一层处理
}

组合优于继承体现在结构体嵌入上:

type ReadWriter struct {
    io.Reader
    io.Writer
}

嵌入后 ReadWriter 自动获得 Reader 和 Writer 的全部方法——用组合替代了多继承,还能随时覆盖单个方法。

九、哪些"经典模式"在 Go 里不需要

  • 模板方法:Java 靠抽象类 + 继承,Go 用接口 + 默认实现的函数组合替代;
  • 抽象工厂:Go 的接口 + 注册表已经覆盖需求,类层次是多余的;
  • 建造者:复杂对象的逐步构建用选项模式替代,更简洁;
  • 命令模式:函数值 + 闭包天然就是"可延迟执行的操作"。

十、总结:Go 风格的设计原则

  1. 接口要小:一个方法起步,接口是行为契约不是类型归类;
  2. 组合优先:嵌入 + 接口组合出行为,别造类继承树;
  3. 函数是一等公民:回调、策略、装饰都能用函数表达,比类轻一个量级;
  4. 命名导出选项:选项模式是 Go 对"配置爆炸"的标准回答;
  5. 并发原语替代锁:能用 channel 表达的就别用锁,能用 atomic 的就别用 mutex。

设计模式解决的是变化与复用的矛盾,Go 用接口、函数值、组合三个语言特性就消化了大部分经典模式。真正的高手写的 Go 代码,模式是"长"出来的而不是"套"上去的——保持接口小、依赖显式、组合自然,模式自然浮现。

读完了?给作者一点支持

本文采用 CC BY 4.0 协议转载 · © 小卒子

评论区加载中…

🔗 相关文章