你听过无数次Go的卖点:协程轻量、通道优雅、调度器能跑10万个并发。这些都没错,但都不是Go构建良好架构的真正杀器。
真正改变系统设计方式的,是一个几乎没人单独拿出来讲的功能:隐式接口满足(implicit interface satisfaction)。它让六边形架构在Go里不像硬凑的模式,而像语言本身的自然延伸。
隐式满足到底是什么
Java里,你得宣誓效忠。一个类实现接口,必须显式声明:
public class PostgresRepo
implements Repository {
@Override
public Entity findById(String id) {
// SQL query
}
@Override
public void save(Entity entity) {
// SQL insert
}
}
实现类导入并命名接口,在源码层面就与接口耦合。如果Repository在另一个模块,编译时就必须把它放进类路径。
Go没有implements关键字。一个类型只要方法对上了,就自动满足接口。仅此而已。
// 在你的领域包
type Repository interface {
FindByID(
ctx context.Context,
id string,
) (Entity, error)
Save(
ctx context.Context,
entity Entity,
) error
}
// 在你的基础设施包——完全不同的目录,
// 完全不同的导入路径
type PostgresRepo struct {
db *sql.DB
}
func (r *PostgresRepo) FindByID(
ctx context.Context,
id string,
) (Entity, error) {
// 实际的SQL查询
return Entity{}, nil
}
func (r *PostgresRepo) Save(
ctx context.Context,
entity Entity,
) error {
// 实际的SQL插入
return nil
}
PostgresRepo从没提过Repository。它只导入要持久化的领域类型,对端口接口零依赖。编译器在任何需要Repository的地方都会接受它,而适配器从未声明过这层关系。
这不是小小的语法糖。它彻底反转了依赖的方向。
为什么这对六边形架构至关重要
六边形架构(端口与适配器)有一条核心规则:依赖指向内侧。领域逻辑坐镇中心,基础设施(数据库、消息队列、HTTP处理器)盘踞边缘。领域通过端口(接口)定义自己需要什么,适配器来实现这些端口。
在Java里,贯彻这条规则需要费点功夫。适配器必须导入端口接口,这个导入建立了从外环到内环的编译期依赖——这正是你想要的。但这也意味着适配器得显式声明implements Repository。一旦接口改名,所有实现都得跟着改。
更棘手的是包结构。Java的惯例是把接口和实现放在同一包或紧密耦合的包中。领域包定义Repository,基础设施包实现它,但基础设施包必须导入领域包。这没问题。可如果你想让领域包完全独立——不依赖任何基础设施包——Java能做到,但需要你刻意为之。编译器不会帮你。一个初级开发者很容易在领域包里误引基础设施类型,破坏架构边界。
Go的隐式满足改变了这场游戏。
领域包定义Repository。基础设施包实现所需方法,但从不导入领域包。编译器在构建时检查匹配,而非在源码层面强制关联。领域保持纯净,基础设施自由演化,双方只在方法签名上达成默契。
这种解耦不是约定俗成,而是编译器强制执行。你不可能在领域包里意外依赖基础设施类型——因为那里根本没什么可导入的。架构边界由语言机制守护,而非靠代码审查和团队自律。
从模式到本能
六边形架构在其他语言里是"我们要不要试试这个模式"。在Go里,它是"事情本来就该这么写"。
当你写下一个接口,你不需要同步修改任何实现文件。当你重构领域模型,基础设施代码不会爆红。当你测试业务逻辑,你可以用字面量结构体当桩,无需Mock框架。
这不是框架带来的便利,是类型系统的思维方式。Go把接口当成描述行为的契约,而非血统证明。满足契约靠的是行动,而非声明。
结果就是:团队新人写出来的代码天然分层,没有培训成本。代码库随时间增长而保持清晰,而非逐渐腐烂。架构意图在每一行代码里可见,无需UML图和Wiki文档来解释。
这才是Go被设计出来的样子。不是让你写更多代码来管理复杂度,而是让正确的结构成为阻力最小的路径。
下次有人跟你吹Go的并发模型,点点头。然后回去写你的领域接口——那才是这门语言真正厉害的地方。
热门跟贴