在 Go 项目的 DDD 分层架构中,Echo Web 框架及其 middleware 应该归属到哪一层?main 方法应如何组织和放置?在 main 中启动 Echo Server 的最佳实践是什么?
📂 典型的 DDD + Echo 项目目录结构
.
├── cmd/
│ └── myapp/
│ └── main.go // 程序入口,启动 echo server
├── internal/
│ ├── domain/ // 领域层,实体 (Entity)、值对象 (Value Object)、领域服务
│ ├── application/ // 应用层,用例逻辑,调用 domain
│ ├── infrastructure/ // 基础设施,数据库、第三方 API、日志等
│ └── interfaces/ // 接口层,HTTP handler,Echo router,middleware
└── pkg/ // 公共库
各层职责
domain
只包含业务核心模型和规则,不依赖外部框架。application
编排用例逻辑(比如「下单」调用库存和支付),依赖 domain。infrastructure
具体的实现(DB、缓存、消息队列、第三方服务),为 domain/application 提供支持。interfaces
- Echo 的
router
和handler
- Echo 的
middleware
(如日志、请求ID、恢复 panic) - handler 里调用 application 层的用例
- Echo 的
middleware 放哪里?
放在 interfaces 层,因为 middleware 主要是 Web 层的适配逻辑。
例如:
internal/interfaces/http/middleware/logger.go
internal/interfaces/http/middleware/recover.go
main.go 里该做什么?
main.go
是程序入口,一般只做:
- 初始化配置(config/env)
- 初始化 logger
- 初始化基础设施(DB、缓存等)
- 组装依赖注入(application -> interfaces)
- 创建 Echo 实例并注册 middleware、路由
- 启动 server
示例:cmd/myapp/main.go
package main
import (
"log"
"myapp/internal/interfaces/http" // 你的 http router
"myapp/internal/infrastructure/db"
)
func main() {
// 1. 初始化基础设施
database, err := db.NewPostgres()
if err != nil {
log.Fatal(err)
}
// 2. 初始化 Echo
e := http.NewServer(database) // 在 interfaces 层组装 Echo
// 3. 启动服务
if err := e.Start(":8080"); err != nil {
log.Fatal(err)
}
}
示例:internal/interfaces/http/server.go
package http
import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"myapp/internal/interfaces/http/handler"
)
func NewServer(db any) *echo.Echo {
e := echo.New()
// 注册全局中间件
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// 注册路由和 handler
h := handler.NewUserHandler(db)
e.GET("/users/:id", h.GetUser)
return e
}
✅ 总结:
echo
和middleware
放在 interfaces 层(因为它们是 I/O 适配)。main.go
放在 cmd/{app}/main.go,只负责组装依赖和启动 Echo server。- middleware 和路由初始化逻辑写在
internal/interfaces/http
下,不要放到domain
或application
。
不过,有人倾向于将 Echo 归类为 Web 框架,并封装在基础设施层,而不是直接暴露给其他层。这样可以降低对具体框架的依赖,从而在未来需要替换为其他框架(如 Gin)时,能够更灵活地进行替换,而不会影响整体架构。
DDD 里面其实有两种常见做法:
- 把 web 框架(Echo/Gin/Fiber)放到 infrastructure 层,对外提供一个抽象接口(比如 HTTPServer 接口),application/handler 只依赖接口,不依赖具体实现。这样就能做到将来替换 web 框架时对上层透明。
- 把 web 框架直接放在 interfaces 层,但这时 interfaces 层就跟具体框架耦合了。如果未来要换框架,interfaces 层会有比较大的改动。
这两种其实各有权衡:
- infrastructure 层更“纯粹”,因为它就是实现技术细节的地方。
- interfaces 层更直观,因为很多人认为 HTTP handler 本身就是“接口适配”。
✅ 如果你坚持把 Echo 放到基础设施层,可以这样组织:
.
├── cmd/
│ └── myapp/
│ └── main.go
├── internal/
│ ├── domain/
│ ├── application/
│ ├── infrastructure/ // Echo server 在这里
│ │ └── web/
│ │ ├── echo.go // Echo 实现
│ │ └── gin.go // 将来可换 Gin
│ └── interfaces/ // 定义 HTTP handler(只暴露接口,不依赖 echo)
└── pkg/
定义一个抽象接口
internal/interfaces/server.go
package interfaces
type HTTPServer interface {
Start(addr string) error
Shutdown() error
}
Echo 实现(基础设施层)
internal/infrastructure/web/echo.go
package web
import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"myapp/internal/interfaces"
)
type EchoServer struct {
e *echo.Echo
}
func NewEchoServer() *EchoServer {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
return &EchoServer{e: e}
}
func (s *EchoServer) RegisterRoutes(register func(e *echo.Echo)) {
register(s.e)
}
func (s *EchoServer) Start(addr string) error {
return s.e.Start(addr)
}
func (s *EchoServer) Shutdown() error {
return s.e.Close()
}
main.go 组装
cmd/myapp/main.go
package main
import (
"log"
"myapp/internal/infrastructure/web"
"myapp/internal/interfaces/http"
)
func main() {
// 基础设施层:echo 实现
server := web.NewEchoServer()
// 接口层:注册 handler 到 echo
server.RegisterRoutes(http.RegisterRoutes)
// 启动
if err := server.Start(":8080"); err != nil {
log.Fatal(err)
}
}
handler 不依赖 Echo
internal/interfaces/http/routes.go
package http
import (
"net/http"
"github.com/labstack/echo/v4"
)
func RegisterRoutes(e *echo.Echo) {
e.GET("/health", func(c echo.Context) error {
return c.String(http.StatusOK, "ok")
})
}
将来要换 Gin,只需要:
- 在
infrastructure/web/gin.go
实现HTTPServer
接口 - 在
main.go
替换web.NewEchoServer()
→web.NewGinServer()
👉 所以严格来说:Echo 属于基础设施层,只是大部分 Go 项目为了简单,直接把它放在 interfaces 层用了。