Go 设计模式实战:从接口组合到选项模式的 Go 风格实现
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))
优点:
- 默认值 + 可选参数并存,调用方不必记参数顺序;
- 向后兼容:新增配置项只需加一个 Option,老调用代码不破坏;
- 可扩展: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 风格的设计原则
- 接口要小:一个方法起步,接口是行为契约不是类型归类;
- 组合优先:嵌入 + 接口组合出行为,别造类继承树;
- 函数是一等公民:回调、策略、装饰都能用函数表达,比类轻一个量级;
- 命名导出选项:选项模式是 Go 对"配置爆炸"的标准回答;
- 并发原语替代锁:能用 channel 表达的就别用锁,能用 atomic 的就别用 mutex。
设计模式解决的是变化与复用的矛盾,Go 用接口、函数值、组合三个语言特性就消化了大部分经典模式。真正的高手写的 Go 代码,模式是"长"出来的而不是"套"上去的——保持接口小、依赖显式、组合自然,模式自然浮现。
读完了?给作者一点支持
🔗 相关文章
Go 并发编程实战:从 Goroutine、Channel 到 Context 的完整指南
系统梳理 Go 并发的核心机制:GMP 调度模型、Channel 与 CSP、select 高级用法、sync 工具箱、Context 生命周期管理,以及 Worker Pool、Pipeline、Fan-in/Fan-out 三大模式,并给出 goroutine 泄漏与数据竞争的排查方法和最佳实践清单。
Redis 高可用架构实战:缓存穿透、击穿、雪崩的全面防御
深入解析缓存穿透、击穿、雪崩三大高发事故的本质,给出空值缓存、布隆过滤器、互斥锁、逻辑过期、随机 TTL、多级缓存等可落地防御方案与高可用演进路径。
构建高质量 RESTful API:设计规范与安全实践
从资源建模、状态码语义、JWT 认证鉴权到接口安全十大防护点,再到版本演进策略,给出可直接落地的 RESTful API 设计与安全实践清单。
评论区加载中…