Go中外观模式应直接组合具体子系统类型而非interface{},字段命名需体现职责,构造函数用选项式初始化并校验nil依赖,错误应透传或用%w包装,测试重点是调用编排而非子系统功能。

Go 语言没有传统面向对象的继承和抽象类,但外观模式(Facade)依然可以清晰实现——核心是用一个结构体封装多个子系统接口,提供简化的统一入口。
为什么 Go 里不用 interface{} 或空接口模拟外观
常见误区是试图用 interface{} 接收任意子系统实例,但这会丢失类型安全和 IDE 支持,且无法静态检查方法调用是否合法。外观的本质是「明确知道子系统有哪些协作组件」,所以应直接组合具体类型。
- 外观结构体字段应为具体子系统类型(如
*UserService、*NotificationService),而非接口或interface{} - 若子系统已定义接口(如
UserRepository),外观可依赖该接口,但初始化时仍需传入具体实现 - 过度抽象接口会导致外观层虚化,反而增加调用链路和 mock 难度
外观结构体如何设计字段与构造函数
字段命名要体现职责,避免泛泛的 sys1、svc;构造函数应校验必要依赖是否非 nil,防止运行时报 panic。
- 字段示例:
userSvc *UserService、emailNotif *EmailNotifier、logger *zap.Logger - 构造函数推荐带选项式初始化(functional options),便于测试时注入 mock:
type AppFacade struct {
userSvc *UserService
notifSvc *EmailNotifier
logger *zap.Logger
}
func NewAppFacade(opts ...FacadeOption) *AppFacade {
f := &AppFacade{}
for _, opt := range opts {
opt(f)
}
return f
}
type FacadeOption func(*AppFacade)
func WithUserService(svc *UserService) FacadeOption {
return func(f *AppFacade) { f.userSvc = svc }
}
- 不建议在构造函数中做 heavy 初始化(如连接数据库),应由各子系统自行管理生命周期
外观方法何时该透传错误,何时该包装错误
外观不是错误拦截器,而是协调者。它不掩盖底层问题,但需统一错误语义,避免调用方处理一堆不同包的错误类型。
动态WEB网站中的PHP和MySQL详细反映实际程序的需求,仔细地探讨外部数据的验证(例如信用卡卡号的格式)、用户登录以及如何使用模板建立网页的标准外观。动态WEB网站中的PHP和MySQL的内容不仅仅是这些。书中还提到如何串联JavaScript与PHP让用户操作时更快、更方便。还有正确处理用户输入错误的方法,让网站看起来更专业。另外还引入大量来自PEAR外挂函数库的强大功能,对常用的、强大的包
立即学习“go语言免费学习笔记(深入)”;
- 子系统返回标准 error(如
userSvc.Create()返回error),外观应直接返回,不强制转成自定义 error - 若需补充上下文(如「创建用户失败:通知服务不可用」),用
fmt.Errorf("...: %w", err)包装,保留原始 error 链 - 避免用
errors.New()丢弃原始 error,否则丢失堆栈和类型断言能力 - 不要在外观里重试网络调用——那是子系统或中间件的责任
测试外观层的关键点
外观本身逻辑简单,重点是验证它是否按预期编排子系统调用顺序和参数传递,而非测试子系统功能。
- 对每个外观方法,用 mock 替换所有依赖字段,检查是否调用了正确方法、传入了预期参数
- 测试错误路径:让某个 mock 返回 error,确认外观是否原样返回或合理包装
- 不测并发安全——除非外观内部维护了共享状态(通常不该有)
- 集成测试另起一层,用真实子系统跑端到端流程,不在外观单元测试中覆盖
外观模式在 Go 中真正容易被忽略的,是它和依赖注入容器的边界:外观负责「怎么调」,DI 容器负责「谁来给」。混用两者(比如让外观自己 new 子系统)会让测试和替换实现变得困难。









