六边形架构在go中依赖目录结构与接口约定实现,核心是领域层只依赖抽象接口、禁止反向依赖,通过显式依赖注入和分层(domain→application→infra→handlers)保障可测试性与多适配器支持。

六边形架构在 Go 里没有标准库支持,靠目录结构和接口约定
Go 语言本身不提供六边形架构(Hexagonal Architecture)的框架或抽象层,它完全依赖开发者通过包组织、接口定义和依赖注入来体现“端口与适配器”思想。关键不是用什么工具,而是谁依赖谁、接口放在哪、实现放哪。
常见错误是把 repository 放在 internal/infra 里却让 domain 包直接 import 它——这违反了依赖方向:领域层必须只依赖抽象(接口),不能感知基础设施细节。
- 所有端口(如
OrderRepository接口)应定义在domain或application层,且该包不能 import 任何 infra 或 handler 相关路径 - 适配器(如
postgresOrderRepo)实现在infra/postgres,它 importdomain并实现其接口 - 应用服务(
OrderService)在application层,只依赖domain接口和自身用到的端口,不碰具体实现
如何组织 Go 模块避免循环依赖
循环依赖是 Go 实现六边形架构时最常卡住的地方,典型报错是 import cycle not allowed。根源往往出在接口位置不对,或者误让 infra 包反向 import application。
一个可落地的分层顺序(从内到外):domain → application → infra → handlers。每层只能 import 内层,不能跨层或反向。
-
domain/:只含类型定义(struct)、值对象、领域接口(PaymentProcessor),不 import 任何其他内部包 -
application/:含用例逻辑(CreateOrderUseCase),依赖domain接口,但不实现它们 -
infra/postgres/:含postgresOrderRepo,importdomain和database/sql,实现OrderRepository -
handlers/http/:含 HTTP 路由和 handler,importapplication和infra,组装依赖并启动
如果发现 infra 需要调用 application 的函数,说明职责错位——应该把该逻辑抽成 domain 接口,或移到 use case 内部。
依赖注入别硬写 new,用构造函数参数传接口
Go 没有 DI 容器,六边形架构的依赖注入必须显式完成。很多人在 handler 里直接 new(postgresOrderRepo),结果 domain 层被 infra 实现污染,测试时无法替换 mock。
正确做法是:所有依赖都通过结构体字段 + 构造函数注入,且字段类型必须是 domain 层定义的接口。
// application/order_service.go
type OrderService struct {
repo domain.OrderRepository // 接口,来自 domain/
}
func NewOrderService(repo domain.OrderRepository) *OrderService {
return &OrderService{repo: repo}
}
// handlers/http/order_handler.go
func RegisterOrderHandler(e *echo.Echo, db *sql.DB) {
repo := postgres.NewOrderRepo(db)
service := application.NewOrderService(repo)
h := &OrderHandler{service: service}
e.POST("/orders", h.Create)
}
这样单元测试时,可以传入 &mockOrderRepo{},无需改业务代码;也避免了全局变量或单例导致的测试污染。
HTTP handler 不是唯一适配器,CLI 和 gRPC 同理
六边形架构的价值在于“同一套 domain + application 可对接多个外部系统”。很多人只写了 HTTP handler 就以为完成了,其实 CLI 命令、gRPC 服务、消息队列消费者都是同等级的适配器。
例如加一个 CLI 命令创建订单:
- 新建
cmd/create_order.go - 它 import
application和infra/postgres,构造OrderService实例 - 解析 flag,调用
service.CreateOrder(...)
注意:CLI 文件里不能出现 echo、gin、http.Request 等 HTTP 专属类型,否则就和端口绑定死了。真正的边界是接口,不是文件位置。
最容易被忽略的是事件驱动场景——比如订单创建后发 Kafka 消息。这时 KafkaPublisher 应作为另一个端口定义在 domain,由 application 在用例中调用,而具体实现(kafkaOrderPublisher)放在 infra/kafka。不是所有“发消息”都该塞进 handler 里。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











