go中真正有用的外观模式是封装多依赖的结构体,用于复用状态与协调生命周期;需显式注入依赖、接收context、透传错误,且不承载业务逻辑。

Go 里真正有用的外观模式,不是为了套设计模式名字,而是当你在 main.go 里反复写 db.Open()、redis.NewClient()、http.NewServeMux() 并手动串联错误处理时,该抽一个统一入口了。
什么时候该用结构体封装,而不是包级函数
结构体是必要的,当你需要复用状态或协调多个有生命周期的依赖时。比如 Web 应用启动后要持续持有数据库连接、缓存客户端、HTTP 服务实例——这些不能每次调用都重建。
- 包级函数(如
app.Start())适合一次性初始化 + 启动,返回一个运行中的服务句柄 - 结构体(如
type AppService struct)适合后续还要反复调用业务方法(如svc.CreateOrder()),且需共享底层依赖 - 如果所有方法都不需要共享状态(比如只是组合几个无状态工具函数),那直接用包级函数更轻量,别硬套结构体
- 结构体字段必须全为小写(如
db *sql.DB),避免意外导出;公开方法才用大写(如CreateOrder())
NewXXX 构造函数必须传入所有必需依赖
外观结构体不是单例工厂,它的行为强依赖注入的实例。一旦在方法里 new 具体类型(比如 &redis.Client{}),就失去可测试性和环境隔离能力。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 构造函数签名要显式列出所有必需依赖:如
func NewAppService(db *sql.DB, cache *redis.Client, mailer Mailer) *AppService - 禁止在方法内部做延迟初始化(如
if f.db == nil { f.db = sql.Open(...) }),这会让测试无法控制依赖 - 可选依赖用指针传入(如
logger *log.Logger),并在方法中判空,但文档需注明缺失影响 - 构造函数可做最小校验:
if db == nil { panic("db is required") },比静默容忍nil更利于早期发现问题
Facade 方法必须接收 context.Context 并统一设超时
外观是调用链起点,若某个子系统方法没接收 context.Context,整个流程就丧失超时和取消能力——这是线上事故常见源头。
- 每个公开方法第一行应创建带超时的子上下文:
ctx, cancel := context.WithTimeout(ctx, 5*time.Second) - 务必
defer cancel(),否则可能泄漏 goroutine - 不要把
context.Background()或context.TODO()塞进子系统调用,那等于放弃控制权 - 超时值不应写死在方法里,而应作为结构体字段(如
timeout time.Duration)在构造时注入,方便测试与环境适配
错误处理:透传、包装、但绝不吞掉原始错误链
外观不是错误翻译器,它不负责把所有错误转成 ErrInternal。用户需要知道到底是库存不足、支付拒付,还是 Redis 连接失败。
- 子系统返回的具体错误(如
sql.ErrNoRows、redis.Nil)应保留并包装:fmt.Errorf("get user: %w", err) - 对特定语义错误可做业务层映射(如把
sql.ErrNoRows转为UserNotFound),但必须用%w保留原始 error - 禁止用
fmt.Errorf("operation failed")这类无信息错误覆盖下游错误 - 不要在外观层重试(如自动重试 HTTP 请求),那是子系统或策略组件的事;外观只负责编排顺序和聚合结果
最容易被忽略的一点:外观结构体本身不承载业务规则。金额计算、库存扣减逻辑仍在对应子服务里,外观只管“谁先调、谁后调、错在哪一步”。一旦开始在 PlaceOrder() 里写 if-else 判断优惠券、写 SQL 查询余额,它就不再是外观,而是上帝对象了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










