在上一节中, 已经大体学习了如何使用 Gin 读写请求. 这一节就是实践了, 完成一个用户业务逻辑处理.git
主要包括如下功能:github
这一节是核心部分, 由于这个项目的主要功能就是在这部分实现的.web
这部分的代码改动很大, 毕竟要完成上述的功能会增长不少代码. 首先, 来看下路由, router.go
里增长了不少路由定义.数据库
u := g.Group("/v1/user")
{
u.GET("", user.List)
u.POST("", user.Create)
u.GET("/:id", user.Get)
u.PUT("/:id", user.Save)
u.PATCH("/:id", user.Update)
u.DELETE("/:id", user.Delete)
}
um := g.Group("/v1/username")
{
um.GET("/:name", user.GetByName)
}
复制代码
从路由定义中咱们能够看到, 用户的建立, 更新, 查询和删除都已经定义了. 另外, 还定义了获取用户列表的功能.json
稍微要解释下的是用户的更新, 这里定义了两种方法, 一种使用 PUT 方法, 另外一个种使用 PATCH 方法. 二者的区别在于, 前者是完整更新, 须要提供全部的 字段, 新的用户数据会完成替换掉旧的用户, 除了 ID 不变. 后者是部分更新, 更为灵活, 只须要提供你想改变的字段就好了.服务器
在定义 API 接口的时候, 一般须要控制版本, 通常状况下, 第一个路由目录都是 版本号. 这里也听从这种最佳实践.并发
全部用户相关的 handler 都定义在 handler/user/
目录下.app
先来看看如何建立新用户.函数
建立新用户的步骤以下:学习
若是从请求中获取参数已经在上一节中介绍过了, 这里使用的模型绑定.
Gin 的模型绑定中也有的校验, 一个经常使用的是指定必要的字段. Gin 自己支持使用 go-playground/validator.v8
进行验证.
我这里使用的是 gopkg.in/go-playground/validator.v9
.
首先在 model/user.go
中定义了用户模型, 包括验证的方法.
// 定义用户的结构
type UserModel struct {
BaseModel
Username string `json:"username" gorm:"column:username;not null" binding:"required" validate:"min=1,max=32"`
Password string `json:"password" gorm:"column:password;not null" binding:"required" validate:"min=5,max=128"`
}
// 验证字段
func (u *UserModel) Validate() error {
validate := validator.New()
return validate.Struct(u)
}
复制代码
在使用模型绑定的时候须要注意区分一点, API 接口须要的结构和数据模型自己 是不同的. 数据模型更可能是指保存在数据库中的结构, 关系到如何设计表结构 和核心数据模型. 而请求中的参数结构体是服务于 API 接口自己的, 即这个接口 须要哪些参数.
能够在 handler/user/user.go
中查看全部的用户 API 接口的结构体.
不少操做都已经封装在了用户模型中, 因此在 handler 中, 通常只须要调用函数, 并判断是否出现错误就好了. 尽可能不要在 handler 中塞入不少代码, 一般只须要 显示出一个清晰的处理流程就好了, 具体的实现放在别的文件中.
加密和存储用户数据的过程很是清晰.
// 加密密码
if err := u.Encrypt(); err != nil {
handler.SendResponse(ctx, errno.New(errno.ErrEncrypt, err), nil)
return
}
// 插入用户到数据库中
if err := u.Create(); err != nil {
handler.SendResponse(ctx, errno.New(errno.ErrDatabase, err), nil)
return
}
复制代码
最后, 将返回用户名做为响应. 至此, 一个建立用户的 handler 就完成了.
用户的删除和基于 ID 或名字查询也比较容易, 再也不细说.
来看一个获取用户列表的接口.
遵循前面讲到的原则, 大段的代码不宜直接放在 handler 中, 这里将具体的实现放在了一个叫作 service 的包中, 具体是 service.ListUser
函数.
其实在定义用户模型的时候已经定义了一个同名的方法从数据库中获取 用户列表和用户总数. 为何不直接使用呢?
这是由于在模型中一般只定义很是通用的函数, 也就是从数据库中取出数据, 不对数据作很是具体的处理. 设计到具体业务的操做, 应该在别的地方处理.
具体看一下 service.ListUser
函数, 主要功能是对从数据库中获取的用户 数据进行扩展, 增长了一些字段, 对应 model.UserInfo
结构体.
// 业务处理函数, 获取用户列表
func ListUser(username string, offset, limit int) ([]*model.UserInfo, uint, error) {
infos := make([]*model.UserInfo, 0)
users, count, err := model.ListUser(username, offset, limit)
if err != nil {
return nil, count, err
}
ids := []uint{}
for _, user := range users {
ids = append(ids, user.ID)
}
wg := sync.WaitGroup{}
userList := model.UserList{
Lock: new(sync.Mutex),
IdMap: make(map[uint]*model.UserInfo, len(users)),
}
errChan := make(chan error, 1)
finished := make(chan bool, 1)
// 并行转换
for _, u := range users {
wg.Add(1)
go func(u *model.UserModel) {
defer wg.Done()
shortId, err := util.GenShortID()
if err != nil {
errChan <- err
return
}
// 更新数据时加锁, 保持一致性
userList.Lock.Lock()
defer userList.Lock.Unlock()
userList.IdMap[u.ID] = &model.UserInfo{
ID: u.ID,
Username: u.Username,
SayHello: fmt.Sprintf("Hello %s", shortId),
Password: u.Password,
CreatedAt: util.TimeToStr(&u.CreatedAt),
UpdatedAt: util.TimeToStr(&u.UpdatedAt),
DeletedAt: util.TimeToStr(u.DeletedAt),
}
}(u)
}
go func() {
wg.Wait()
close(finished)
}()
// 等待完成
select {
case <-finished:
case err := <-errChan:
return nil, count, err
}
for _, id := range ids {
infos = append(infos, userList.IdMap[id])
}
return infos, count, nil
}
复制代码
实际上, 为了加速处理过程, 使用了 goroutine 进行并行处理:
在 ListUser() 函数中用了 sync 包来作并行查询,以使响应延时更小。在实际开发中,查询数据后,一般须要对数据作一些处理,好比 ListUser() 函数中会对每一个用户记录返回一个 sayHello 字段。sayHello 只是简单输出了一个 Hello shortId 字符串,其中 shortId 是经过 util.GenShortId() 来生成的(GenShortId 实现详见 demo07/util/util.go)。像这类操做一般会增长 API 的响应延时,若是列表条目过多,列表中的每一个记录都要作一些相似的逻辑处理,这会使得整个 API 延时很高,因此笔者在实际开发中一般会作并行处理。根据笔者经验,效果提高十分明显。
读者应该已经注意到了,在 ListUser() 实现中,有 sync.Mutex 和 IdMap 等部分代码,使用 sync.Mutex 是由于在并发处理中,更新同一个变量为了保证数据一致性,一般须要作锁处理。
使用 IdMap 是由于查询的列表一般须要按时间顺序进行排序,通常数据库查询后的列表已经排过序了,可是为了减小延时,程序中用了并发,这时候会打乱排序,因此经过 IdMap 来记录并发处理前的顺序,处理后再从新复位。
里面用到的知识点还挺多的, 涉及到了 goroutine, 锁与同步, range, select , chanel. 我觉有能够多看几遍体会一下.
在更新用户的时候提供了两种方式, 彻底更新与部分更新, 分别对应 PUT 和 PATCH.
对于用户模型而言, GORM 下的操做是很方便的.
// 保存用户, 会更新全部的字段
func (u *UserModel) Save() error {
return DB.Self.Save(u).Error
}
// 更新字段, 使用 map[string]interface{} 格式
func (u *UserModel) Update(data map[string]interface{}) error {
return DB.Self.Model(u).Updates(data).Error
}
复制代码
重点在于获取数据和验证的阶段.
对于彻底更新, 其实除了 ID 是已知的, 其余部分和建立用户时一致的, 一样是验证字段并加密密码, 最后更新数据库.
对于部分更新, 咱们就须要去猜想传递过了的字段, 并对每种字段一一进行处理. 新写了一个验证方法 ValidateAndUpdateUser
.
// ValidateAndUpdateUser 验证 map 结构, 并加密密码(若是存在的话)
func ValidateAndUpdateUser(data *map[string]interface{}) error {
validate := validator.New()
usernameTag, _ := util.GetTag(UserModel{}, "Username", "validate")
passwordTag, _ := util.GetTag(UserModel{}, "Password", "validate")
// 验证 username
if username, ok := (*data)["username"]; ok {
if err := validate.Var(username, usernameTag); err != nil {
return err
}
}
// 验证 password
if password, ok := (*data)["password"]; ok {
if err := validate.Var(password, passwordTag); err != nil {
return err
}
// 加密密码
newPassword, err := auth.Encrypt(password.(string))
if err != nil {
return err
}
(*data)["password"] = newPassword
}
return nil
}
复制代码
对于每种字段都验证了一遍, 感受有点繁琐.
用户的核心逻辑就是这些了. 粗看起来这部分的代码改动是很是多的. 到此为止, 大部分的核心代码已经完成了, 这个 API 服务器算是 可以启动了, 并接收调用了.
固然, 还有许多地方还没完善, 好比权限认证, 接口文档等, 都会在接下来的文章中一一介绍.
做为版本 v0.7.0