不能直接照搬官方示例目录结构,因其缺乏分层设计,导致接口膨胀时权限校验分散、事务边界模糊、测试困难;企业级项目需五层分包(cmd/、internal/app/、handlers/、services/、repositories/)明确职责,依赖注入保障可测性与可维护性。

为什么不能直接照搬官方示例的目录结构
因为 echo.New() 启动一个空服务很简单,但企业级项目要应对接口膨胀、团队协作、CI/CD、配置隔离和可观测性——官方示例里把 handler、model、db 全塞进 main.go 或平铺在根目录,上线后改个日志格式都要 grep 十几个文件。
真实痛点是:新加一个「用户导出 Excel」接口,结果发现权限校验逻辑散在 middleware、handler 和 service 里三份;数据库事务边界模糊,sql.Tx 在哪层开始、在哪层 commit 没共识;测试时想 mock 数据库,却要重写整个 handlers.UserHandler 实例。
- 不要把
models当成纯 struct 定义目录——它该包含领域行为(如User.HashPassword()),而不是只放type User struct{} - 避免
services层直接 importdatabase/sql或调用db.QueryRow()——这会让单元测试被迫启数据库 -
handlers必须无状态:不持有*sql.DB、不缓存用户 session,所有依赖通过构造函数注入
推荐的五层分包结构(含职责边界)
不是为了分层而分层,而是让每个变更影响范围可预期。比如改 Redis 缓存策略,只动 cache 包和 services 里调用它的那几个方法。
cmd/ —— 只有 main.go,负责初始化 config、logger、DB 连接池,然后把构建好的 *echo.Echo 交给 app.Run()。这里不写任何业务逻辑。
立即学习“go语言免费学习笔记(深入)”;
internal/app/ —— 应用入口,含 app.New()(组装依赖)、app.RegisterRoutes()(绑定 handler)。路由定义集中在此,不分散到各 handler 文件。
internal/handlers/ —— 纯 HTTP 转换层:解析 c.Param()、c.Bind(),调用 services,返回 c.JSON()。禁止出现 if err != nil { log.Fatal() },错误统一由中间件处理。
internal/services/ —— 业务核心。每个 service 对应一个聚合根(如 UserSvc),方法名体现意图:UserSvc.CreateUser() 而非 UserSvc.Insert()。事务控制在这里显式管理(tx, _ := db.BeginTx())。
Colly 是一个用于 Go 语言的快速开源爬取和爬虫框架。它适用于从简单的页面提取到异步爬虫处理大量页面集合,支持请求回调和结构化解析。
internal/repositories/ —— 数据访问契约。定义 UserRepo interface,具体实现(如 postgres.UserRepo)放在 internal/infrastructure/postgres/。这样换 MySQL 或加 Mock 实现只需替换实现,不改 service。
config 和 logger 的初始化陷阱
常见错误是把 viper.ReadInConfig() 放在 main.go 里,导致 handlers 或 services 包里到处用 viper.GetString("db.host") —— 这违反了依赖倒置,也使测试无法注入不同配置。
- 在
cmd/main.go中读取配置,构造成config.Configstruct(字段全小写,带json:tag),传给app.New(config) - logger 不要用全局变量
log包,用zerolog.Logger或zap.Logger,通过依赖注入传入 handler/service。否则并发场景下log.SetOutput()会互相覆盖 - 环境区分靠 build tag 而非
viper.SetEnvPrefix():运行go run -tags=prod cmd/main.go,在config/prod.go里硬编码生产密钥(或从 secret manager 加载)
测试组织与依赖注入实践
企业项目最常卡在「写不了测试」——因为 handler 直接 new 了 DB 连接,service 里 new 了 logger,mock 成本高过重写。
关键动作只有两个:所有外部依赖必须作为参数传入构造函数,interface 定义放在被依赖方的包里(即 repositories.UserRepo interface 定义在 internal/repositories/,而非 internal/services/)。
例如 services/user_service.go:
type UserService struct {
userRepo repositories.UserRepo
logger zerolog.Logger
}
func NewUserService(repo repositories.UserRepo, logger zerolog.Logger) *UserService {
return &UserService{userRepo: repo, logger: logger}
}
测试时:
repo := &mockUserRepo{} // 实现 repositories.UserRepo 接口
svc := services.NewUserService(repo, zerolog.Nop())
// 调用 svc.CreateUser(...),断言 repo.Create 被调用一次
真正难的是坚持——哪怕临时加个 debug 日志,也要把 logger 作为参数传进去,而不是偷偷 import log 包。

















