Go 中桥接模式应使用显式接口字段组合而非 embed:Circle 结构体持 renderer Renderer 字段,NewCircle 注入具体实现,Draw 调用 renderer.RenderCircle,接口按能力切分如 DrawLine、SetColor,状态同步需谨慎设计。

Go 里没有“桥接模式”的标准实现模板,照搬 Java/UML 类图只会写出反模式代码。真正有效的桥接,是用 interface 定义契约、用结构体字段持有实现、靠组合完成解耦——不是继承,也不是 embed。
为什么不能用 embed + interface 做桥接
有人写 type Shape struct { Renderer },以为匿名字段能实现运行时切换。这是错的:
- embed 是编译期语法糖,字段名省略了,但底层类型固定,无法在运行时替换为另一个实现
- 如果
Renderer接口有RenderCircle(),而Shape自己也有同名方法,编译器会静默覆盖,调用行为不可控 -
Shape会意外暴露Renderer的全部方法,API 被污染,违背单一职责 - 无法对字段做空值检查、日志打点或单元测试 mock —— 你得构造完整实现,而不是传个轻量结构体或函数
正确写法:显式字段 + 接口注入
抽象层结构体必须显式声明接口字段,构造时注入具体实现。例如:
<pre class="brush:php;toolbar:false;">type Renderer interface {
RenderCircle(x, y, r float64)
SetColor(color string)
}
type Circle struct {
x, y, r float64
renderer Renderer // 明确字段名,可读、可控、可测试
}
func NewCircle(r Renderer) *Circle {
return &Circle{renderer: r}
}
func (c *Circle) Draw() {
c.renderer.RenderCircle(c.x, c.y, c.r)
}
- 初始化时传入
&VectorRenderer{} 或 <code>&RasterRenderer{},切换实现只需换参数 -
Draw()方法内不出现if-else判断渲染器类型,也不做type assertion - 所有字段必须显式初始化,否则
c.renderer为nil,调用RenderCircle()会 panic
接口设计要窄但覆盖桥接点
接口不是越宽越好,也不是越细越安全。关键看是否支撑“正交变化”:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 避免按图形类型命名方法:如
RenderCircle()、RenderRect()—— 新增图形就得改接口,所有实现同步改 - 推荐按底层能力切分:
DrawLine(LineSpec)、DrawPoint(Point)、SetColor(color string) - 参数尽量用结构体封装,别堆一堆
float64:比如type LineSpec struct { X1, Y1, X2, Y2 float64; Width int } - 若某实现不支持某个操作(如 SVG 渲染器不支持抗锯齿),应在该实现内部返回明确错误,而非让抽象层处理 fallback
容易被忽略的复杂点:状态同步与生命周期
桥接本身不自动同步状态。比如 Circle 可能维护坐标系变换,Renderer 可能维护 DPI、抗锯齿开关、当前颜色等。这些状态彼此独立,但业务上可能需要联动:
- 不要在
Circle.Draw()里手动同步renderer.SetDPI()—— 这等于把桥接逻辑又塞回抽象层 - 更合理的做法是让
Renderer自行管理状态,或通过额外配置结构体(如RenderOptions)一次性传入 - 若需跨桥接层共享上下文(如 trace ID、超时控制),应通过参数传递,而非依赖字段或全局变量
真正难的从来不是怎么写接口,而是想清楚哪些状态属于抽象层、哪些属于实现层、哪些该由调用方负责传递。

















