现在,我们就把上一章建立的“饭店后厨”模型,进行一次真实的“开火预演”。
我将沿用之前的风格,将这些代码片段串成一个完整的故事,并在此过程中进行评估、优化,补充一些关键的“幕后细节”,让整个流程更加清晰和健壮。
1 后厨实战:当一个“用户注册”请求抵达后…
理论讲了这么多,是时候看看我们的“后厨团队”如何处理一份真实的订单了!这份订单就是——“新用户注册”。
我们将完整追踪这份订单从“服务员”接单,到“总厨”策划,再到“仓库”存档的全过程,看看我们精心设计的“责任链”是如何优雅运作的。
2 “餐厅开业” - 在 main 中组建团队
在处理任何订单之前,餐厅老板(main函数)必须在开业时把团队组建好,并明确每个人的“汇报关系”。这就是依赖注入的“组装”阶段。
您原来的 main.go 中 initUser 函数的逻辑非常清晰,但它更适合住在“后勤中心” ioc 包里,而不是待在 main.go 这个“大堂”。我们来优化一下,让 ioc 包名副其实。
不必关注 initUser 函数的所在包位置,这里仅作为展示如何从底层 (dao) 开始,一步步创建实例并向上“注入”依赖,最终组装出一个完整的 UserHandler。initWebServer 的逻辑也同理。
优化建议:
- 将这些初始化函数(“团队组建说明书”)全部挪到
ioc 包中。main.go 应该只关心“调用说明书来组建团队”,而不关心“说明书”的具体内容。这让 main.go 保持干净,职责更单一。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
func main() {
db := ioc.InitDB()
udl := initUser(db)
server := initWebServer(udl)
server.Run(":8021")
}
func initWebServer(userHdl *web.UserHandler) *gin.Engine {
server := gin.Default()
userHdl.RegisterRoutes(server)
return server
}
func initUser(db *gorm.DB) *web.UserHandler {
ud := dao.NewUserDAO(db)
repo := repository.NewUserRepository(ud)
svc := service.NewUserService(repo)
u := web.NewUserHandler(svc)
return u
}
|
3 “订单”处理流程
现在,团队组建完毕,一位新客人发来 POST /users/signup 请求,订单来了!
第 1 站:web.UserHandler - “服务员”接单
“服务员”是第一个接触到订单(HTTP 请求)的角色。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
// internal/web/user.go
package web
import (
"net/http"
"einscat.com/user-mgr/internal/domain"
"einscat.com/user-mgr/internal/service"
"github.com/gin-gonic/gin"
)
type UserHandler struct {
// 服务员(Handler)只认识总厨(Service)
svc *service.UserService
}
func NewUserHandler(svc *service.SERVICE.UserService) *UserHandler {
return &UserHandler{
svc: svc,
}
}
// RegisterRoutes 负责将 Handler 的各个“上菜”方法注册到路由上
func (h *UserHandler) RegisterRoutes(server *gin.Engine) {
ug := server.Group("/users")
ug.POST("/signup", h.SignUp)
}
// SignUp 处理注册请求
func (h *UserHandler) SignUp(ctx *gin.Context) {
type SignUpReq struct {
NickName string `json:"nickname"`
PhoneNumber string `json:"phone_number"`
Password string `json:"password"`
}
var req SignUpReq
// 1. 服务员检查菜单(请求体),看客人是否填对了
if err := ctx.BindJSON(&req); err != nil {
// 如果填错了,直接告诉客人,不用麻烦后厨
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request parameters"})
return
}
// 2. 将客人的需求翻译成“后厨通用语言”(domain.User)
err := h.svc.SignUp(ctx, domain.User{
NickName: req.NickName,
PhoneNumber: req.PhoneNumber,
Password: req.Password,
})
// 3. 根据总厨的处理结果,给客人一个回复
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Signup failed"})
return
}
ctx.JSON(http.StatusOK, gin.H{"message": "Signup successful"})
}
|
解读:
UserHandler 通过构造函数 NewUserHandler 接收了 UserService 实例,它只知道有这么个“总厨”可以使唤。
SignUp 方法做了三件事:解析并校验前端传来的 JSON -> 调用 service 层处理核心业务 -> 根据 service 的返回结果,给前端一个标准的 HTTP 响应。
第 2 站:service.UserService - “总厨”决策
“总厨”接到了“服务员”递来的标准订单,他需要根据“菜谱”(业务逻辑)来决策。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
// internal/service/user.go
package service
import (
"context"
"einscat.com/user-mgr/internal/domain"
"einscat.com/user-mgr/internal/repository"
)
type UserService struct {
// 总厨(Service)只认识仓库管理员(Repository)
repo *repository.UserRepository
}
func NewUserService(repo *repository.UserRepository) *UserService {
return &UserService{
repo: repo,
}
}
func (svc *UserService) SignUp(ctx context.Context, u domain.User) error {
// 菜谱第一步:加密用户密码(重要的业务逻辑!)
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
if err != nil {
return err
}
u.Password = string(hashedPassword)
// 菜谱第二步:通知仓库管理员,把这个新用户信息存档
return svc.repo.Create(ctx, u)
}
|
解读:
- 对于一个简单的注册流程,
service 的核心业务逻辑可能就是密码加密、参数校验等。
- 它不关心数据最终存在 MySQL 还是 Redis,它只信任
repository 这个“仓库管理员”,并把 domain.User 这个“后厨通用对象”交给他。
- 方法中添加 context 是为了保持链路与超时控制。
既然是注册用户,就需要传入用户的注册信息,所以 SignUp 还需要接收下用户的注册信息。
上面提到web下的 handler只能使用service层的东西,但是serviec不能使用 web层的内容,这里需要接收用户信息,就需要定义一个User相关的内容,所以这里在与 web、service同级的地方定义个domain包,在里面定义用户的领域对象,然后在 SignUp 方法中接收一个领域对象。
第 3 站:repository.UserRepository - “仓库管理员”的翻译工作
这是至关重要的一环!“仓库管理员”是连接 业务世界(domain)和数据世界(dao) 的桥梁。
在 Repository 层还完成了 domain.User 到 dao.User 的转换。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
// internal/repository/user.go
package repository
import (
"context"
"einscat.com/user-mgr/internal/domain"
"einscat.com/user-mgr/internal/repository/dao"
)
type UserRepository struct {
// 仓库管理员(Repository)认识档案室的办事员(DAO)
dao *dao.UserDAO
}
func NewUserRepository(dao *dao.UserDAO) *UserRepository {
return &UserRepository{
dao: dao,
}
}
// Create 负责创建用户
func (r *UserRepository) Create(ctx context.Context, u domain.User) error {
// 核心职责:将 domain 对象 翻译成 dao 对象
return r.dao.Insert(ctx, r.toDAO(u))
}
// toDAO 是一个私有辅助函数,负责转换
func (r *UserRepository) toDAO(u domain.User) dao.User {
return dao.User{
Id: u.Id,
NickName: u.NickName,
PhoneNumber: u.PhoneNumber,
Password: u.Password,
// Ctime 在 DAO 层面可以通过 gorm 的 hook 自动填充
}
}
|
第 4 站:dao.UserDAO - “档案室办事员”的物理操作
“办事员”是最终的执行者,他只认识和数据库打交道的“档案卡”(dao.User),并使用专业的工具 (gorm) 将其存档。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
// internal/repository/dao/user.go
package dao
import (
"context"
"time"
"gorm.io/gorm"
)
type UserDAO struct {
db *gorm.DB
}
func NewUserDAO(db *gorm.DB) *UserDAO {
return &UserDAO{
db: db,
}
}
// Insert 将一个 dao.User 对象插入数据库
func (dao *UserDAO) Insert(ctx context.Context, u User) error {
// 使用带有超时的上下文,确保数据库操作不会永远阻塞
return dao.db.WithContext(ctx).Create(&u).Error
}
// User 结构体严格对应数据库的 users 表结构
// 这是一个 PO (Persistent Object)
type User struct {
Id uint64 `gorm:"primaryKey,autoIncrement"`
NickName string
PhoneNumber string
Password string
CreatedAt time.Time
UpdatedAt time.Time
}
|
解读:
dao.User 结构体现在使用了 gorm 标签,它和 users 表的字段一一对应。
Insert 方法接收的是 dao.User,职责非常纯粹:就是把它存进数据库。
至此,一个“用户注册”请求就走完了它的完整旅程。每层各司其其职,互不干扰,通过依赖注入紧密协作,构成了一套清晰、健壮、易于测试和维护的系统。