重构
我们将代码拆分为三层,各司其职:
- Handler 层 (类似 Controller):只负责接待客人。解析 HTTP 请求 (Gin),校验参数,然后把活儿派给 Service。
- Service 层 (业务逻辑):负责做饭。这里是核心业务逻辑(比如积分计算、发邮件),它不关心你是 HTTP 来的还是命令行来的。
- Repository/Model 层:负责拿食材。只管和数据库打交道(我们目前直接在 Service 里调 DB,后续项目大了再拆 Repository)。
第一步:创建 Service 层 (业务逻辑)
这一层是你的核心。我们要定义一个“用户服务”。
创建文件:internal/service/user_service.go
Go
package service
import (
"user-system/internal/model"
"user-system/pkg/database"
)
// 1. 定义接口 (Contract)
// 这是一个“契约”,规定了用户服务必须具备的功能。
// 为什么要写接口?为了解耦和方便后续写单元测试(Mock)。
type UserService interface {
CreateUser(user *model.User) error
}
// 2. 定义结构体 (Implementation)
type userService struct {
// 这里可以放其他的依赖,比如 database.DB,但目前我们直接引用全局的
}
// 3. 构造函数 (Factory)
// 返回接口类型,这样外部只知道我有这个功能,不关心我怎么实现的
func NewUserService() UserService {
return &userService{}
}
// 4. 实现接口方法
func (s *userService) CreateUser(user *model.User) error {
// 这里是纯粹的业务逻辑
// 比如:可以在这里加一句 "user.Points += 10" (注册送积分)
return database.DB.Create(user).Error
}第二步:创建 Handler 层 (HTTP 接待)
这一层负责把 Gin 的 Context 转换成 Service 能懂的参数。
创建文件:internal/handler/user_handler.go
Go
package handler
import (
"net/http"
"user-system/internal/model"
"user-system/internal/service"
"github.com/gin-gonic/gin"
)
// UserHandler 持有 Service 的引用
type UserHandler struct {
svc service.UserService
}
// NewUserHandler 构造函数
// 依赖注入:你需要把 service 传给我,我才能工作
func NewUserHandler(svc service.UserService) *UserHandler {
return &UserHandler{
svc: svc,
}
}
// CreateUser 处理 POST /users 请求
func (h *UserHandler) CreateUser(c *gin.Context) {
var user model.User
// 1. 参数校验 (和之前一样)
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 2. 调用 Service (业务逻辑)
// Handler 不碰数据库,只调 Service
if err := h.svc.CreateUser(&user); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 3. 返回结果
c.JSON(http.StatusOK, gin.H{
"message": "User created successfully",
"data": user,
})
}第三步:清理并组装 Main (入口)
现在回到 cmd/api-server/main.go。你会发现 main 函数变得异常清爽,它只负责组装零件。
修改代码:
Go
package main
import (
"fmt"
"user-system/internal/handler"
"user-system/internal/model"
"user-system/internal/service"
"user-system/pkg/database"
"github.com/gin-gonic/gin"
)
func main() {
// 1. 初始化数据库
dsn := "root:123456@tcp(127.0.0.1:3306)/user_db?charset=utf8mb4&parseTime=True&loc=Local"
database.InitMySQL(dsn)
database.DB.AutoMigrate(&model.User{})
// 2. 初始化各层 (依赖注入/组装)
// 就像搭积木:Service 依赖 DB(这里隐式了),Handler 依赖 Service
userService := service.NewUserService()
userHandler := handler.NewUserHandler(userService)
// 3. 初始化 Gin
r := gin.Default()
// 4. 注册路由
// 注意:这里我们传入的是 userHandler.CreateUser 这个方法
r.POST("/users", userHandler.CreateUser)
// 5. 启动
fmt.Println("Starting server on :8080...")
r.Run(":8080")
}🛑 架构师的“避坑指南”
在这次重构中,有两个 Go 语言特有的坑,我必须指出来:
1. 循环依赖 (Import Cycle) —— 新手必死之坑
- 场景:假如你在
service包里import "handler",又在handler包里import "service"。 - 结果:Go 编译器会直接报错
import cycle not allowed。 - 架构原则:依赖必须是单向的。
- Handler ->依赖-> Service ->依赖-> Model
- 绝不能反过来! Service 不能知道 Handler 的存在。
2. 小写字母的可见性
- 注意看
userService(结构体) 是小写的,而UserService(接口) 是大写的。 - 这意味着:外部(main 包)不能直接创建
&service.userService{}(因为它是私有的)。 - 强制规范:外部必须通过
service.NewUserService()这个工厂方法来创建。这是一种很好的封装习惯,防止外部乱改结构体内部字段。