Skip to content

重构

我们将代码拆分为三层,各司其职:

  1. Handler 层 (类似 Controller):只负责接待客人。解析 HTTP 请求 (Gin),校验参数,然后把活儿派给 Service。
  2. Service 层 (业务逻辑):负责做饭。这里是核心业务逻辑(比如积分计算、发邮件),它不关心你是 HTTP 来的还是命令行来的。
  3. 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() 这个工厂方法来创建。这是一种很好的封装习惯,防止外部乱改结构体内部字段。