main.go 不该直接 import 数据库驱动,否则会导致 handler 与 infra 层耦合,破坏 Clean Architecture 的依赖规则(外层不可依赖内层具体实现),使单元测试无法 mock、数据库切换成本高。

为什么 main.go 不该直接 import 数据库驱动
因为这会让 handler 层和 infra 层耦合,导致单元测试无法 mock 数据访问、切换数据库时要改一堆业务代码。Clean Architecture 要求依赖只能指向内层,外层(如 HTTP handler)可以依赖内层接口,但不能依赖具体实现(比如 github.com/lib/pq 或 gorm.io/gorm)。
实操建议:
- 在
internal/infra下定义UserRepository接口,放在 domain 层或 repo 层(推荐 domain 层声明接口,infra 层实现) -
internal/adapter/http/handler.go只依赖internal/domain.UserRepository,不 import 任何 driver 包 - 初始化时用构造函数注入:把
*sql.DB或*gorm.DB实例传给 infra 层的实现,再把实现传给 handler - 别在 handler 里调
sql.Open()—— 连接池创建属于 infra 初始化职责,不是 API 响应逻辑
domain 层里放什么、绝对不能放什么
domain 层是整个架构的“心脏”,只包含业务规则和核心实体,它必须能脱离框架、数据库、HTTP 单独编译和测试。
常见错误现象:编译报错 import cycle not allowed,往往是因为 domain 引用了 echo.Context、http.Request 或 gorm.Model。
立即学习“go语言免费学习笔记(深入)”;
实操建议:
- 允许:结构体(如
User)、方法(如user.Activate())、接口(如UserRepository)、自定义错误类型(如ErrUserNotFound) - 禁止:
"net/http"、"github.com/labstack/echo/v4"、"gorm.io/gorm"、"encoding/json"(除非是纯数据结构序列化逻辑且不依赖外部行为) - 时间处理统一用
time.Time,别用postgres.NullTime或mysql.MySQLTime - 如果需要验证逻辑(如邮箱格式),写成 domain 方法,不要依赖
validatortag —— tag 是 infra 层适配器的事
怎么让 UseCase 真正隔离业务逻辑
UseCase 不是“服务层”的别名,它的唯一职责是协调 domain 实体与 repository 接口,不处理 HTTP 状态码、JSON 序列化、中间件逻辑。
容易踩的坑:把 c.JSON(200, res) 写进 UseCase;或者在 UseCase 里手动拼 SQL;又或者让 UseCase 直接返回 *gorm.DB。
实操建议:
- UseCase 函数签名应该像这样:
func (uc *UserUseCase) GetByID(ctx context.Context, id string) (*domain.User, error) - 错误返回统一用 domain 自定义 error,而不是
fmt.Errorf("db error: %w", err)—— 后者会把 infra 细节泄露出去 - 不要在 UseCase 里调
log.Printf或zap.Sugar().Info—— 日志是 adapter 层或 main 层的事 - 如果有跨 repository 操作(如转账需扣 A 账户、增 B 账户),UseCase 可以接收多个 repository 接口,但不能知道它们底层是否共享事务 —— 那是 infra 层的实现细节
为什么 internal/adapter 下还要分 http 和 postgres
因为同一套 domain + usecase 可能同时跑在 HTTP、gRPC、CLI 甚至定时任务里;数据库也可能从 PostgreSQL 切到 SQLite 测试,或加一层 Redis 缓存。分得清,才能换得动。
性能影响明显:把 HTTP 解析逻辑(如 binding、validation)混进 UseCase,会导致单元测试不得不伪造 echo.Context;把 SQL 查询写死在 handler,就根本没法做内存 mock 测试。
实操建议:
-
internal/adapter/http只做三件事:解析请求、调用 UseCase、格式化响应(status code / header / JSON marshal) -
internal/adapter/postgres只做两件事:实现 domain 定义的 repository 接口、管理具体 SQL 或 ORM 映射(含事务控制) - 避免出现
internal/adapter/http/postgres这种嵌套路径 —— 它暴露了你把传输层和数据层当成一个东西在维护 - 如果要用 gorm,
internal/adapter/postgres/user_repository.go里可以有db.WithContext(ctx).First(&u, "id = ?", id),但 domain 层的UserRepository接口里只能有FindByID(id string) (*domain.User, error)
最常被忽略的一点:repository 接口方法名要面向业务,而不是 SQL 动词。别叫 CreateUserRecord,叫 Store 或 Save;别叫 GetUserBySQLWhere,叫 FindByEmail —— 后者才体现领域语义,前者只是技术实现的倒影。










