很多人刚接触 Go 时,会本能地把 Java 的设计模式(GoF 23 种)套进去,结果写出一堆复杂的结构体 + 接口 + 工厂,结果发现:Go 里很多经典模式要么简化了,要么干脆不需要了。
为什么?因为 Go 是静态类型 + 编译期决定一切的语言,它缺少 Java 的“动态特性”(如运行时反射、继承、动态代理等),但也正是这些缺失,让 Go 避免了很多“为了绕弯子而发明”的模式。
下面用最常见的几个模式举例,告诉你 Go 是怎么“偷懒”却更简洁的。
1. 单例模式(Singleton) → 几乎不需要
Java 中经典单例:
public class Logger {
private static final Logger instance = new Logger();
private Logger() {
}
public static Logger getInstance() {
return instance; }
}
Go 里直接用包级变量 + init():
package logger
var std = &Logger{
}
func init() {
std = NewLogger() // 或从配置加载
}
func Std() *Logger {
return std
}
或者更简单:全局变量就够了(在小型服务中很常见)。
为什么 Go 不需要复杂单例?
Go 是静态链接的二进制,没有类加载器、没有反射动态创建。包一旦导入,变量就存在了。并发安全用 sync.Once 包一下就行。
var (
once sync.Once
instance *Logger
)
func GetLogger() *Logger {
once.Do(func() {
instance = NewLogger()
})
return instance
}
2. 工厂模式(Factory) → 通常就是普通函数
Java 里:
interface Transport {
void deliver(); }
class Truck implements Transport {
... }
class Ship implements Transport {
... }
class TransportFactory {
public static Transport create(String type) {
if ("truck".equals(type)) return new Truck();
if ("ship".equals(type)) return new Ship();
throw new IllegalArgumentException();
}
}
Go 里直接返回接口:
type Transport interface {
Deliver()
}
func NewTransport(typ string) Transport {
switch typ {
case "truck":
return &Truck{
}
case "ship":
return &Ship{
}
default:
return nil // 或 panic / error
}
}
更地道做法:直接用具名构造函数,不需要工厂类。
func NewTruck() Transport {
return &Truck{
} }
func NewShip() Transport {
return &Ship{
} }
因为 Go 接口是隐式实现,调用方不需要知道具体类型。
3. 策略模式(Strategy) → 接口 + 函数类型就够
Java 需要一堆类:
interface PaymentStrategy {
void pay(int amount); }
class CreditCard implements PaymentStrategy {
... }
class PayPal implements PaymentStrategy {
... }
Go 里函数本身可以是第一等公民:
type PayFunc func(amount int) error
func ProcessPayment(pay PayFunc, amount int) error {
return pay(amount)
}
// 使用
ProcessPayment(func(amount int) error {
fmt.Println("Paying with credit card:", amount)
return nil
}, 100)
或者用接口(但通常不需要一堆 struct)。
4. 装饰器模式(Decorator) → 嵌入 + 组合
Java 需要一堆 wrapper 类。
Go 直接用嵌入(embedding):
type Logger interface {
Log(msg string)
}
type ConsoleLogger struct{
}
func (c ConsoleLogger) Log(msg string) {
fmt.Println(msg) }
type TimedLogger struct {
Logger // 嵌入
}
func (t TimedLogger) Log(msg string) {
fmt.Printf("[%s] ", time.Now().Format(time.Kitchen))
t.Logger.Log(msg)
}
一句话总结:Go 用组合优于继承 + 嵌入,天然支持装饰。
5. 观察者模式(Observer) → channel + goroutine 更常见
Java 用一堆 Listener 接口。
Go 里直接用 channel 广播:
type EventBus struct {
subs map[string][]chan string
mu sync.RWMutex
}
func (b *EventBus) Subscribe(topic string) <-chan string {
ch := make(chan string, 10)
b.mu.Lock()
b.subs[topic] = append(b.subs[topic], ch)
b.mu.Unlock()
return ch
}
func (b *EventBus) Publish(topic, msg string) {
b.mu.RLock()
for _, ch := range b.subs[topic] {
ch <- msg
}
b.mu.RUnlock()
}
更简单场景:直接用 chan 就完事。
核心原因总结:Go 的“静态”让很多模式消失
| 特性 | Java(动态/反射重) | Go(静态/简单) | 结果 |
|---|---|---|---|
| 接口实现 | 必须显式 implements | 隐式实现(duck typing) | 少写 adapter、proxy |
| 继承 | 广泛使用 | 没有继承,只有嵌入 + 组合 | 少用 decorator、template method |
| 函数一等公民 | 较晚支持 lambda | 原生支持闭包、方法值 | 策略、命令、visitor 简化 |
| 反射/动态代理 | 强大(Spring AOP 等) | 几乎不用(性能优先) | 少用 proxy、动态工厂 |
| 包级作用域 | 没有(public/protected 等) | 有(大写导出,小写私有) | 少用 singleton、utility 类 |
| 并发原语 | Thread + synchronized | goroutine + channel + sync 包 | observer、pub-sub 用 channel 代替 |
一句话结论:
设计模式是语言缺陷的补丁。Go 通过简洁的语法(接口隐式、组合优先、函数一等、channel 原语)把很多“经典问题”直接消灭了,所以你写 Go 代码时,少想模式,多想简单。
记住 Rob Pike 的名言:
“Less is exponentially more.”
写 Go 的时候,先问自己:能不能用一个函数 / 一个接口 / 一个 channel 解决?
能的话,就别硬套工厂、单例、装饰器了。
Happy Go coding!🚀