使用 Go 完成用戶業務邏輯

簡介

在上一節中, 已經大體學習瞭如何使用 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 都定義在 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 就完成了.

其餘 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

相關文章
相關標籤/搜索