Go 優(yōu)秀實(shí)踐:請(qǐng)求參數(shù)校驗(yàn)方法設(shè)計(jì)和實(shí)現(xiàn)
本節(jié)課會(huì)詳細(xì)介紹下 gRPC 請(qǐng)求的請(qǐng)求參數(shù)校驗(yàn)邏輯實(shí)現(xiàn)。
一、為什么要 API 接口請(qǐng)求參數(shù)
對(duì) API 請(qǐng)求參數(shù)進(jìn)行校驗(yàn)是 Web 開(kāi)發(fā)中需要實(shí)現(xiàn)的一個(gè)核心功能之一,它不僅能夠提升系統(tǒng)的可靠性,還可以提高用戶體驗(yàn)、數(shù)據(jù)安全性以及代碼的可維護(hù)性。以下是具體原因的介紹:
(1) 保證系統(tǒng)的穩(wěn)定性:API 接收到的請(qǐng)求參數(shù)可能從客戶端或第三方應(yīng)用發(fā)起,這些參數(shù)可能由于客戶端開(kāi)發(fā)錯(cuò)誤、意外修改或惡意構(gòu)造而不符合預(yù)期。如果未對(duì)請(qǐng)求參數(shù)進(jìn)行校驗(yàn),可能導(dǎo)致系統(tǒng)邏輯錯(cuò)誤,甚至出現(xiàn)程序崩潰,從而影響服務(wù)的可用性。例如:未校驗(yàn)分頁(yè)參數(shù),可能引發(fā)數(shù)據(jù)庫(kù)查詢性能急劇下滑,如負(fù)數(shù)頁(yè)碼或極大的 limit;
(2) 確保數(shù)據(jù)的合法性和完整性:無(wú)論是前端用戶輸入還是對(duì)接方系統(tǒng)調(diào)用,都有可能提交不符合業(yè)務(wù)要求的數(shù)據(jù),如必填字段缺失、字符串格式不正確、超出預(yù)期范圍等。如果直接寫(xiě)入數(shù)據(jù)庫(kù)或業(yè)務(wù)邏輯處理,可能會(huì)產(chǎn)生錯(cuò)誤數(shù)據(jù),導(dǎo)致后續(xù)問(wèn)題難以排查;
(3) 增強(qiáng)用戶體驗(yàn):不進(jìn)行參數(shù)校驗(yàn),錯(cuò)誤通常會(huì)發(fā)生在邏輯處理階段(如存儲(chǔ)數(shù)據(jù)庫(kù)層或業(yè)務(wù)邏輯層),錯(cuò)誤提示可能與用戶的實(shí)際問(wèn)題無(wú)關(guān),而是以難以理解的系統(tǒng)錯(cuò)誤呈現(xiàn)。這不僅難以定位問(wèn)題,還會(huì)讓用戶感到困惑。通過(guò)校驗(yàn)參數(shù),可以在第一時(shí)間返回清晰的錯(cuò)誤信息,告訴用戶問(wèn)題所在,改善用戶體驗(yàn)。例如"username"字段為空時(shí)提示:"用戶名不能為空";
(4) 維護(hù)代碼的清晰性和可維護(hù)性:沒(méi)有參數(shù)校驗(yàn)的代碼通常需要開(kāi)發(fā)者在業(yè)務(wù)邏輯部分反復(fù)進(jìn)行參數(shù)檢查,例如空值判斷、格式驗(yàn)證、一層層的數(shù)據(jù)過(guò)濾,這會(huì)導(dǎo)致業(yè)務(wù)邏輯代碼雜亂且難以維護(hù)。通過(guò)集中化參數(shù)校驗(yàn):
- 將參數(shù)校驗(yàn)邏輯從業(yè)務(wù)邏輯中剝離,保持代碼簡(jiǎn)潔;
- 參數(shù)檢查可以在控制器層完成,使核心業(yè)務(wù)處理代碼得到解耦。
(5) 服務(wù)端可信性原則:在開(kāi)發(fā)中,應(yīng)遵循“永遠(yuǎn)不要完全信任客戶端”的原則。即使在客戶端已做校驗(yàn)(如前端的表單必填檢查),也必須在后端進(jìn)行校驗(yàn)。
對(duì) API 請(qǐng)求參數(shù)進(jìn)行校驗(yàn),最核心的目的是提升系統(tǒng)的健壯性和安全性,提供良好的用戶體驗(yàn)并減少錯(cuò)誤傳播。在 Go 項(xiàng)目開(kāi)發(fā)中,服務(wù)端必須對(duì)所有來(lái)自客戶端的數(shù)據(jù)進(jìn)行嚴(yán)格校驗(yàn),確保系統(tǒng)處于受控狀態(tài)。
二、API 接口請(qǐng)求參數(shù)校驗(yàn)方法
API 接口請(qǐng)求參數(shù)校驗(yàn)方法有多種。本節(jié)會(huì)介紹這些校驗(yàn)方法,并結(jié)合真實(shí)場(chǎng)景下的請(qǐng)求參數(shù)校驗(yàn)需求,實(shí)現(xiàn) miniblog 的請(qǐng)求參數(shù)校驗(yàn)方法。具體來(lái)說(shuō),有以下幾種請(qǐng)求參數(shù)校驗(yàn)方法:
- 手動(dòng)校驗(yàn);
- 第三方校驗(yàn)庫(kù);
- 使用 Web 框架內(nèi)置校驗(yàn)功能;
- 基于工具生成校驗(yàn)代碼;
- 中間件校驗(yàn)。
在實(shí)際開(kāi)發(fā)中,不少開(kāi)發(fā)者會(huì)同時(shí)使用上述校驗(yàn)方法中的兩種或更多種,導(dǎo)致項(xiàng)目的校驗(yàn)方式不夠規(guī)范和統(tǒng)一,從而增加了代碼閱讀的難度,降低了開(kāi)發(fā)效率,并提高了維護(hù)成本。導(dǎo)致同時(shí)使用多種校驗(yàn)方式的原因有多方面,例如項(xiàng)目缺乏統(tǒng)一的校驗(yàn)規(guī)范,開(kāi)發(fā)者隨意選擇自己偏好的校驗(yàn)方法,或者現(xiàn)有的校驗(yàn)方式在形式和功能上無(wú)法完全滿足項(xiàng)目的實(shí)際需求。
所以,miniblog 項(xiàng)目結(jié)合實(shí)際 Go 項(xiàng)目開(kāi)發(fā)中的業(yè)務(wù)校驗(yàn)場(chǎng)景,設(shè)計(jì)一種更加通用和標(biāo)準(zhǔn)化的 API 接口請(qǐng)求參數(shù)校驗(yàn)方法。
1. 手動(dòng)校驗(yàn)
手動(dòng)校驗(yàn)指的是直接在代碼中判斷參數(shù)是否合法。這種方法適用于簡(jiǎn)單的項(xiàng)目,不需要引入額外工具或包,但維護(hù)成本較高,不適合復(fù)雜的項(xiàng)目。
代碼清單 10-1 展示了一個(gè)手動(dòng)校驗(yàn)的代碼示例。
代碼清單 10-1 手動(dòng)校驗(yàn):
package main
import (
"errors"
"fmt"
)
type LoginRequest struct {
Username string
Password string
}
func validate(req LoginRequest) error {
if req.Username == "" {
return errors.New("username is required")
}
if len(req.Password) < 6 {
return errors.New("password must be at least 6 characters long")
}
return nil
}
func main() {
req := LoginRequest{Username: "user", Password: "12345"}
if err := validate(req); err != nil {
fmt.Println("Validation failed:", err)
return
}
fmt.Println("Validation passed")
}
2. 第三方校驗(yàn)庫(kù)
Go 項(xiàng)目有許多成熟且功能強(qiáng)大的第三方參數(shù)校驗(yàn)庫(kù),這些校驗(yàn)庫(kù)根據(jù)結(jié)構(gòu)體標(biāo)簽來(lái)進(jìn)行字段校驗(yàn)。例如常用的校驗(yàn)庫(kù)包括:go-playground/validator(常用)、asaskevich/govalidator、ozzo-validation 等。
這些庫(kù)提供了豐富的校驗(yàn)規(guī)則(如必填字段、正則表達(dá)式、數(shù)值范圍等),還支持自定義規(guī)則并可自動(dòng)處理嵌套結(jié)構(gòu)體。
代碼清單 10-2 展示了使用使用 go-playground/validator 進(jìn)行請(qǐng)求參數(shù)校驗(yàn)的代碼示例。
代碼清單 10-2 第三方校驗(yàn)庫(kù):
package main
import (
"fmt"
"github.com/go-playground/validator/v10"
)
type LoginRequest struct {
Username string `validate:"required"` // 必填字段
Password string `validate:"required,min=6"` // 最小長(zhǎng)度為6
Email string `validate:"required,email"` // 必填且必須是郵箱格式
}
func main() {
validate := validator.New() // 創(chuàng)建驗(yàn)證器
req := LoginRequest{
Username: "user",
Password: "12345",
Email: "invalid-email",
}
// 校驗(yàn)結(jié)構(gòu)體
err := validate.Struct(req)
if err != nil {
// 獲取校驗(yàn)錯(cuò)誤并打印
for _, err := range err.(validator.ValidationErrors) {
fmt.Printf("Field '%s' failed validation, rule '%s'\n", err.Field(), err.Tag())
}
} else {
fmt.Println("Validation passed!")
}
}
使用第三方驗(yàn)證庫(kù)校驗(yàn),優(yōu)點(diǎn)是可以直接復(fù)用現(xiàn)成的校驗(yàn)邏輯,并且直接基于結(jié)構(gòu)體標(biāo)簽來(lái)進(jìn)行驗(yàn)證,更加高效,代碼更加簡(jiǎn)潔。但缺點(diǎn)是缺乏靈活性,難以滿足復(fù)雜的校驗(yàn)場(chǎng)景。
3. 使用 Web 框架內(nèi)置校驗(yàn)功能
Gin 框架支持結(jié)合 go-playground/validator 的校驗(yàn),在處理請(qǐng)求數(shù)據(jù)時(shí),利用 binding 標(biāo)簽可以直接解析和校驗(yàn)。示例代碼如代碼清單 10-3 所示。
代碼清單 10-3 使用 Web 框架內(nèi)置功能:
package main
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
)
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required,min=6"`
}
func main() {
r := gin.Default()
r.POST("/login", func(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
// 返回校驗(yàn)錯(cuò)誤
errs := err.(validator.ValidationErrors)
c.JSON(http.StatusBadRequest, gin.H{"error": errs.Error()})
return
}
// 校驗(yàn)通過(guò)
c.JSON(http.StatusOK, gin.H{"message": "Login successful"})
})
r.Run()
}
使用 Web 框架自帶的校驗(yàn)功能,簡(jiǎn)單便捷,但無(wú)法滿足復(fù)雜的校驗(yàn)場(chǎng)景。
4. 基于工具生成校驗(yàn)代碼
在一些大型項(xiàng)目中,可以使用工具自動(dòng)生成校驗(yàn)規(guī)則(例如基于 OpenAPI/Swagger 的定義),通過(guò)自動(dòng)化的方式生成校驗(yàn)邏輯,減少手動(dòng)編寫(xiě)的工作量,提高開(kāi)發(fā)效率和代碼一致性。常用的工具包括:
- OpenAPI Generator:支持根據(jù) OpenAPI 描述生成 Go 代碼,包括參數(shù)校驗(yàn)邏輯;
- gqlgen(GraphQL 工具):自動(dòng)生成 API 代碼,其中包含參數(shù)校驗(yàn)等功能。
使用工具生成校驗(yàn)代碼優(yōu)點(diǎn)是簡(jiǎn)單便捷,開(kāi)發(fā)工作量小。但缺點(diǎn)也是無(wú)法滿足真實(shí)企業(yè)應(yīng)用開(kāi)發(fā)中,遇到的復(fù)雜校驗(yàn)場(chǎng)景。
5. 中間件校驗(yàn)
在某些情況下,可以將校驗(yàn)邏輯抽象為中間件處理,比如 Token 校驗(yàn)、權(quán)限校驗(yàn)、固定格式的參數(shù)校驗(yàn)等,例如:
func ValidationMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-API-Key") == "" {
http.Error(w, "Missing API Key", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
三、miniblog 請(qǐng)求參數(shù)校驗(yàn)設(shè)計(jì)
在實(shí)際的 Go 項(xiàng)目開(kāi)發(fā)中,對(duì)于接口請(qǐng)求參數(shù)校驗(yàn)方法的的一般訴求如下:
- 支持自定義復(fù)雜校驗(yàn)邏輯:能夠根據(jù)具體需求定義復(fù)雜的參數(shù)校驗(yàn)規(guī)則。這些規(guī)則可能超出簡(jiǎn)單的數(shù)據(jù)長(zhǎng)度或大小校驗(yàn)的范疇,例如需要通過(guò)查詢數(shù)據(jù)庫(kù)驗(yàn)證記錄是否存在,或依賴(lài)與第三方微服務(wù)的交互來(lái)完成復(fù)雜的校驗(yàn)邏輯;
- 復(fù)用已有的參數(shù)校驗(yàn)邏輯:支持將某個(gè)參數(shù)的校驗(yàn)邏輯封裝并復(fù)用。例如,用戶密碼的校驗(yàn)邏輯在創(chuàng)建用戶時(shí)需要用到,修改用戶密碼時(shí)同樣適用。這種情況下,校驗(yàn)規(guī)則應(yīng)在不同接口間保持一致性,避免重復(fù)實(shí)現(xiàn);
- 靈活通用的校驗(yàn)方式:允許根據(jù)不同場(chǎng)景靈活調(diào)整校驗(yàn)邏輯,使請(qǐng)求參數(shù)校驗(yàn)更具通用性,適應(yīng)多樣化的需求場(chǎng)景,提升開(kāi)發(fā)效率與代碼維護(hù)性。
- 校驗(yàn)方式簡(jiǎn)單易維護(hù):校驗(yàn)方式需要簡(jiǎn)單,并且容易維護(hù)。
基于上述需求,miniblog 項(xiàng)目設(shè)計(jì)了以下請(qǐng)求參數(shù)校驗(yàn)方案:
- 校驗(yàn)方式易維護(hù):項(xiàng)目中所有 API 請(qǐng)求參數(shù)校驗(yàn)邏輯集中保存在 internal/apiserver/pkg/validation 目錄下。不同資源的校驗(yàn)邏輯保存在不同的源碼文件中,便于查閱和維護(hù)各資源的校驗(yàn)邏輯。
- 校驗(yàn)方式標(biāo)準(zhǔn)化:所有請(qǐng)求接口的校驗(yàn)函數(shù)聲明為統(tǒng)一的規(guī)范格式,例如:Validate<請(qǐng)求參數(shù)結(jié)構(gòu)體名>(ctx context.Context, rq *apiv1.<請(qǐng)求參數(shù)結(jié)構(gòu)體名>) error;
- 支持自定義校驗(yàn)邏輯:通過(guò)創(chuàng)建專(zhuān)門(mén)的校驗(yàn)類(lèi)型,將數(shù)據(jù)庫(kù)連接、第三方微服務(wù)客戶端、緩存客戶端等依賴(lài)實(shí)例注入到校驗(yàn)類(lèi)型的實(shí)例中。在自定義校驗(yàn)邏輯中,使用這些依賴(lài)實(shí)例,進(jìn)行復(fù)雜的邏輯校驗(yàn);
- 支持靈活的校驗(yàn)方法:既支持復(fù)雜的自定義校驗(yàn)邏輯,又支持復(fù)用某個(gè)請(qǐng)求參數(shù)的校驗(yàn)邏輯。
因?yàn)檎?qǐng)求參數(shù)校驗(yàn),幾乎是每個(gè)接口都需要的功能,所以最理想的情況是通過(guò) Web 中間件來(lái)校驗(yàn)請(qǐng)求參數(shù)?;诖嗽O(shè)計(jì)思路,設(shè)計(jì)了 miniblog 的請(qǐng)求參數(shù)校驗(yàn)方案,如圖 10-2 所示。
圖 10-2 請(qǐng)求參數(shù)校驗(yàn)設(shè)計(jì):
圖 10-2 中,定義一個(gè) Validator 結(jié)構(gòu)體類(lèi)型,結(jié)構(gòu)體類(lèi)型中包含了自定義請(qǐng)求參數(shù)校驗(yàn)需要的各類(lèi)依賴(lài)項(xiàng)。Validator 結(jié)構(gòu)體類(lèi)型包含了格式如 ValidateXXX(ctx context.Context, rq *apiv1.XXX) error 的請(qǐng)求參數(shù)校驗(yàn)方法,用來(lái)對(duì)名為 XXX 的請(qǐng)求參數(shù)結(jié)構(gòu)體類(lèi)型進(jìn)行校驗(yàn)。為了提高項(xiàng)目的可維護(hù)性,建議 XXX 的命名格式為 <接口名>Request,例如 Login 接口的參數(shù)校驗(yàn)方法為:ValidateLoginRequest(ctx context.Context, rq *apiv1.LoginRequest) error。
圖 10-2 中,封裝了一個(gè)通用校驗(yàn)層,通用校驗(yàn)層會(huì)解析 Validator 類(lèi)型的實(shí)例,遍歷該實(shí)例中的所有方法,并提取出格式為 ValidateXXX(ctx context.Context, rq *apiv1.XXX) error 的方法,將這些方法保存在一個(gè) map 類(lèi)型的變量中,鍵為請(qǐng)求參數(shù)結(jié)構(gòu)體名,值為校驗(yàn)方法本身。
Web 中間件層,通過(guò)通用校驗(yàn)層來(lái)對(duì)接口進(jìn)行驗(yàn)證。在校驗(yàn)請(qǐng)求參數(shù)時(shí),根據(jù)請(qǐng)求參數(shù)類(lèi)型名,從通用校驗(yàn)層中查找鍵為類(lèi)型名的鍵值對(duì),并調(diào)用值(校驗(yàn)方法)進(jìn)行參數(shù)校驗(yàn)。
通用校驗(yàn)層提供了 ValidateAllFields(obj any, rules Rules) error 函數(shù),該函數(shù)支持復(fù)用某個(gè)請(qǐng)求參數(shù)的校驗(yàn)邏輯,下文會(huì)詳細(xì)介紹。
四、miniblog 請(qǐng)求參數(shù)校驗(yàn)實(shí)現(xiàn)
上一節(jié)介紹了 miniblog 項(xiàng)目的請(qǐng)求參數(shù)校驗(yàn)設(shè)計(jì)方案。本節(jié)將詳細(xì)說(shuō)明 miniblog 是如何實(shí)現(xiàn)這些校驗(yàn)方案的。
miniblog 項(xiàng)目同時(shí)支持基于 Gin 框架的 HTTP 服務(wù)器和基于 gRPC 框架的 RPC 服務(wù)器。由于兩種服務(wù)器類(lèi)型在請(qǐng)求處理中間件層能獲取到的請(qǐng)求信息不同,因此在實(shí)現(xiàn)請(qǐng)求參數(shù)校驗(yàn)邏輯時(shí)也有所區(qū)別。
1. 實(shí)現(xiàn)請(qǐng)求參數(shù)校驗(yàn)方法
在 internal/apiserver/pkg/validation/validation.go 文件中,定義了 Validator 結(jié)構(gòu)體類(lèi)型,該類(lèi)型包含了自定義校驗(yàn)邏輯中需要的各類(lèi)依賴(lài)項(xiàng),以及用來(lái)校驗(yàn)請(qǐng)求參數(shù)的各類(lèi)校驗(yàn)方法。Validator 結(jié)構(gòu)體類(lèi)型定義如下:
// Validator 是驗(yàn)證邏輯的實(shí)現(xiàn)結(jié)構(gòu)體.
type Validator struct {
// 有些復(fù)雜的驗(yàn)證邏輯,可能需要直接查詢數(shù)據(jù)庫(kù)
// 這里只是一個(gè)舉例,如果驗(yàn)證時(shí),有其他依賴(lài)的客戶端/服務(wù)/資源等,
// 都可以一并注入進(jìn)來(lái)
store store.IStore
}
Post 資源相關(guān)接口的請(qǐng)求參數(shù)校驗(yàn)方法實(shí)現(xiàn)位于 internal/apiserver/pkg/validation/post.go 文件中,校驗(yàn)方法實(shí)現(xiàn)如代碼清單 10-4 所示。
代碼清單 10-4 Post 資源請(qǐng)求參數(shù)校驗(yàn)方法實(shí)現(xiàn):
// ValidateCreatePostRequest 校驗(yàn) CreatePostRequest 結(jié)構(gòu)體的有效性.
func (v *Validator) ValidateCreatePostRequest(ctx context.Context, rq *apiv1.CreatePostRequest) error {
return genericvalidation.ValidateAllFields(rq, v.ValidatePostRules())
}
// ValidateUpdatePostRequest 校驗(yàn)更新用戶請(qǐng)求.
func (v *Validator) ValidateUpdatePostRequest(ctx context.Context, rq *apiv1.UpdatePostRequest) error {
return genericvalidation.ValidateAllFields(rq, v.ValidatePostRules())
}
// ValidateDeletePostRequest 校驗(yàn) DeletePostRequest 結(jié)構(gòu)體的有效性.
func (v *Validator) ValidateDeletePostRequest(ctx context.Context, rq *apiv1.DeletePostRequest) error {
return genericvalidation.ValidateAllFields(rq, v.ValidatePostRules())
}
// ValidateGetPostRequest 校驗(yàn) GetPostRequest 結(jié)構(gòu)體的有效性.
func (v *Validator) ValidateGetPostRequest(ctx context.Context, rq *apiv1.GetPostRequest) error {
return genericvalidation.ValidateAllFields(rq, v.ValidatePostRules())
}
// ValidateListPostRequest 校驗(yàn) ListPostRequest 結(jié)構(gòu)體的有效性.
func (v *Validator) ValidateListPostRequest(ctx context.Context, rq *apiv1.ListPostRequest) error {
if rq.Title != nil && len(rq.Title) > 200 {
return errno.ErrInvalidArgument.WithMessage("title cannot be longer than 200 characters")
}
return genericvalidation.ValidateSelectedFields(rq, v.ValidatePostRules(), "Offset", "Limit")
}
代碼清單 10-4 實(shí)現(xiàn)了 Post 資源 CreatePost、UpdatePost、GetPost、ListPost 接口的請(qǐng)求參數(shù)校驗(yàn)邏輯。
ValidateAllFields 函數(shù)用來(lái)對(duì)請(qǐng)求參數(shù)中的所有字段進(jìn)行校驗(yàn),其中每個(gè)字段的校驗(yàn)規(guī)則在 ValidatePostRules 方法中設(shè)置。ValidatePostRules 方法實(shí)現(xiàn)如下:
// Validate 校驗(yàn)字段的有效性.
func (v *Validator) ValidatePostRules() genericvalidation.Rules {
// 定義各字段的校驗(yàn)邏輯,通過(guò)一個(gè) map 實(shí)現(xiàn)模塊化和簡(jiǎn)化
return genericvalidation.Rules{
"PostID": func(value any) error {
if value.(string) == "" {
return errno.ErrInvalidArgument.WithMessage("postID cannot be empty")
}
return nil
},
"Title": func(value any) error {
if value.(string) == "" {
return errno.ErrInvalidArgument.WithMessage("title cannot be empty")
}
return nil
},
"Content": func(value any) error {
if value.(string) == "" {
return errno.ErrInvalidArgument.WithMessage("content cannot be empty")
}
return nil
},
}
}
代碼清單 10-4 的 ValidateListPostRequest 方法調(diào)用了 ValidateSelectedFields 函數(shù),該函數(shù)只會(huì)校驗(yàn)傳入的字段 Offset、Limit。apiv1.ListPostRequest 結(jié)構(gòu)體中其他字段,例如 Title 字段的校驗(yàn),可以自行實(shí)現(xiàn)校驗(yàn)邏輯,通過(guò)這種方式,允許開(kāi)發(fā)者根據(jù)需要選擇,哪些字段使用通用的字段校驗(yàn)規(guī)則校驗(yàn),哪些字段自行實(shí)現(xiàn)校驗(yàn)邏輯,以此滿足復(fù)雜的字段校驗(yàn)邏輯。
這里要注意,如果指定了校驗(yàn) NonExist 字段,但 NonExist 字段沒(méi)有在 apiv1.ListPostRequest 結(jié)構(gòu)體存在,則 ValidateSelectedFields 函數(shù)會(huì)跳過(guò) NonExist 字段的校驗(yàn)。
另外,ValidateAllFields、ValidateSelectedFields 函數(shù)在校驗(yàn)時(shí),如果結(jié)構(gòu)體中的某個(gè)字段不存在對(duì)應(yīng)的校驗(yàn) Rule,則函數(shù)會(huì)跳過(guò)該字段的校驗(yàn)。通過(guò)給字段(例如 PostID、Title、Content)指定相同的校驗(yàn)規(guī)則,來(lái)保證不同 API 接口相同字段的校驗(yàn)邏輯一致性。
2. HTTP 請(qǐng)求參數(shù)校驗(yàn)
在 Gin 中間件中,無(wú)法提前獲知 API 的請(qǐng)求參數(shù)類(lèi)型,所以無(wú)法實(shí)現(xiàn)在中間件中對(duì)請(qǐng)求參數(shù)進(jìn)行校驗(yàn)。請(qǐng)求參數(shù)的校驗(yàn),在路由函數(shù)中實(shí)現(xiàn)。
在 internal/apiserver/server.go 文件中添加以下代碼創(chuàng)建請(qǐng)求參數(shù)校驗(yàn)實(shí)例,代碼如下:
import (
...
"github.com/onexstack/miniblog/internal/apiserver/pkg/validation"
...
)
...
// ServerConfig 包含服務(wù)器的核心依賴(lài)和配置.
type ServerConfig struct {
val *validation.Validator
}
...
// NewServerConfig 創(chuàng)建一個(gè) *ServerConfig 實(shí)例.
// 進(jìn)階:這里其實(shí)可以使用依賴(lài)注入的方式,來(lái)創(chuàng)建 *ServerConfig.
func (cfg *Config) NewServerConfig() (*ServerConfig, error) {
...
return &ServerConfig{
...
val: validation.New(store),
}, nil
}
在創(chuàng)建 HTTP Handler 時(shí),傳入請(qǐng)求參數(shù)校驗(yàn)實(shí)例,代碼如下:
func (c *ServerConfig) InstallRESTAPI(engine *gin.Engine) {
...
// 創(chuàng)建核心業(yè)務(wù)處理器
handler := handler.NewHandler(c.biz, c.val)
...
}
在 HTTP Handler 層的方法中傳入請(qǐng)求參數(shù)校驗(yàn)方法。例如,ListPost 接口 Handler 層代碼實(shí)現(xiàn)如下:
// ListPosts 列出用戶的所有博客帖子.
func (h *Handler) ListPost(c *gin.Context) {
core.HandleQueryRequest(c, h.biz.PostV1().List, h.val.ValidateListPostRequest)
}
調(diào)用 core.HandleQueryRequest 函數(shù)時(shí),顯式會(huì)傳入校驗(yàn)方法 ValidateListPostRequest。
3. gRPC 請(qǐng)求參數(shù)校驗(yàn)
gRPC 接口的請(qǐng)求參數(shù)校驗(yàn)統(tǒng)一通過(guò) gRPC 攔截器實(shí)現(xiàn)。
在 internal/apiserver/grpcserver.go 文件中,新增以下代碼,用來(lái)在攔截器鏈中添加請(qǐng)求參數(shù)校驗(yàn)攔截器:
import (
...
genericvalidation "github.com/onexstack/onexstack/pkg/validation"
...
)
...
func (c *ServerConfig) NewGRPCServerOr() (server.Server, error) {
serverOptions := []grpc.ServerOption{
// 注意攔截器順序!
grpc.ChainUnaryInterceptor(
...
mw.ValidatorInterceptor(genericvalidation.NewValidator(c.val)),
),
}
...
}
上述代碼用 genericvalidation.NewValidator 函數(shù)創(chuàng)建通用校驗(yàn)層實(shí)例。創(chuàng)建通用校驗(yàn)層實(shí)例時(shí),會(huì)解析傳入的請(qǐng)求參數(shù)校驗(yàn)實(shí)例 c.val,NewValidator 函數(shù)會(huì)從實(shí)例中提取出所有方法聲明格式為 ValidateXXX(ctx context.Context, rq *apiv1.XXX) error 的方法,并保存在通用校驗(yàn)層的內(nèi)部 registry 中。
ValidatorInterceptor 攔截器實(shí)現(xiàn)如下:
// ValidatorInterceptor 是一個(gè) gRPC 攔截器,用于對(duì)請(qǐng)求進(jìn)行驗(yàn)證.
func ValidatorInterceptor(validator RequestValidator) grpc.UnaryServerInterceptor {
return func(ctx context.Context, rq any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
// 調(diào)用自定義驗(yàn)證方法
if err := validator.Validate(ctx, rq); err != nil {
// 注意這里不用返回 errno.ErrInvalidArgument 類(lèi)型的錯(cuò)誤信息,由 validator.Validate 返回.
return nil, err // 返回驗(yàn)證錯(cuò)誤
}
// 繼續(xù)處理請(qǐng)求
return handler(ctx, rq)
}
}
在 ValidatorInterceptor 攔截器中,會(huì)調(diào)用通用校驗(yàn)層實(shí)例的 Validate 方法,Validate 方法實(shí)現(xiàn)代碼如下所示:
// Validate validates the request using the appropriate validation method.
func (v *Validator) Validate(ctx context.Context, request any) error {
validationFunc, ok := v.registry[reflect.TypeOf(request).Elem().Name()]
if !ok {
return nil // No validation function found for the request type
}
result := validationFunc.Call([]reflect.Value{reflect.ValueOf(ctx), reflect.ValueOf(request)})
if !result[0].IsNil() {
return result[0].Interface().(error)
}
return nil
}
Validate 方法會(huì)從通用校驗(yàn)層實(shí)例的 registry 中查找鍵為 gRPC 接口請(qǐng)求參數(shù)結(jié)構(gòu)體名稱(chēng)(例如 LoginRequest)的記錄。如果找到,說(shuō)明該請(qǐng)求參數(shù)結(jié)構(gòu)體已經(jīng)指定了自定義的請(qǐng)求參數(shù)校驗(yàn)方法,執(zhí)行注冊(cè)的校驗(yàn)方法進(jìn)行請(qǐng)求參數(shù)校驗(yàn)。否則,不執(zhí)行校驗(yàn)邏輯。
至此,請(qǐng)求參數(shù)校驗(yàn)代碼開(kāi)發(fā)完成,完整代碼見(jiàn) feature/s22 分支。
四、請(qǐng)求處理測(cè)試
至此,我們已經(jīng)實(shí)現(xiàn)了 miniblog 的核心邏輯。本節(jié)就來(lái)測(cè)試下這些功能是否正常可用。測(cè)試內(nèi)容包括以下幾部分:
- 接口測(cè)試:測(cè)試健康檢查接口、用戶接口、博客接口是否可以正常工作;
- 請(qǐng)求處理功能測(cè)試:測(cè)試請(qǐng)求參數(shù)默認(rèn)值設(shè)置、請(qǐng)求參數(shù)校驗(yàn)功能是否可用。
1. 接口測(cè)試
為了方便讀者測(cè)試功能,miniblog 項(xiàng)目已經(jīng)提前編寫(xiě)好了接口測(cè)試代碼。運(yùn)行以下命令來(lái)分別來(lái)測(cè)試健康檢查接口、用戶接口、博客接口。
修改 $HOME/.miniblog/mb-apiserver.yaml 文件,將 server-mode 設(shè)置為 grpc-gateway。
打開(kāi)一個(gè) Linux 終端,運(yùn)行以下命令啟動(dòng) mb-apiserver 服務(wù):
$ make build BINS=mb-apiserver
$ _output/platforms/linux/amd64/mb-apiserver
打開(kāi)另一個(gè) Linux 終端,運(yùn)行以下命令分別測(cè)試健康檢查接口、用戶接口、博客接口:
$ go run examples/client/health/main.go # 測(cè)試健康檢查接口
{"timestamp":"2025-02-01 16:38:08"}
$ go run examples/client/user/main.go # 測(cè)試用戶相關(guān)接口
2025/02/01 16:38:22 [CreateUser ] Success to create user, userID: user-die7iy
...
2025/02/01 16:38:22 [DeleteUser ] Success to delete user: user-die7iy
2025/02/01 16:38:22 [All ] Success to test all user api
$ go run examples/client/post/main.go # 測(cè)試博客相關(guān)接口
2025/02/01 16:38:51 [CreateUser ] Success to create user, userID: user-die7iy
...
2025/02/01 16:38:51 [All ] Success to test all post api
2025/02/01 16:38:51 [Login ] Success to login with root account
運(yùn)行上述測(cè)試代碼,日志輸出中沒(méi)有錯(cuò)誤,說(shuō)明接口功能正常。
2. 請(qǐng)求處理功能測(cè)試
運(yùn)行以下命令測(cè)試請(qǐng)求處理功能是否正常工作:
$ go run examples/client/reqprocess/main.go # 測(cè)試請(qǐng)求處理功能
2025/02/01 16:39:17 [CreateUser ] Success to create user, userID: user-die7iy
2025/02/01 16:39:17 [Login ] Success to login
2025/02/01 16:39:17 [GetUser ] Success in testing request parameter default value setting
2025/02/01 16:39:17 [GetUser ] Success in testing request parameter validation
五、小結(jié)(AI 自動(dòng)生成并人工審核)
本文詳細(xì)介紹了在 Go 項(xiàng)目開(kāi)發(fā)中如何實(shí)現(xiàn) API 接口請(qǐng)求參數(shù)的校驗(yàn)邏輯,并以 miniblog 項(xiàng)目為例進(jìn)行了實(shí)踐。
文章首先闡述了對(duì)請(qǐng)求參數(shù)進(jìn)行校驗(yàn)的重要性,強(qiáng)調(diào)其在提升系統(tǒng)穩(wěn)定性、確保數(shù)據(jù)合法性、增強(qiáng)用戶體驗(yàn)以及提高代碼可維護(hù)性等方面的作用。隨后,文章分析了常見(jiàn)的參數(shù)校驗(yàn)方法,包括手動(dòng)校驗(yàn)、第三方庫(kù)校驗(yàn)、框架內(nèi)置校驗(yàn)、工具生成校驗(yàn)代碼以及中間件校驗(yàn)等,并指出實(shí)際開(kāi)發(fā)中可能因使用多種校驗(yàn)方式導(dǎo)致的規(guī)范性問(wèn)題。
基于此,miniblog 項(xiàng)目設(shè)計(jì)了一種標(biāo)準(zhǔn)化、靈活且易維護(hù)的參數(shù)校驗(yàn)方案,采用統(tǒng)一的校驗(yàn)接口格式,并通過(guò)通用校驗(yàn)層實(shí)現(xiàn)了復(fù)雜校驗(yàn)邏輯的支持和復(fù)用。
具體實(shí)現(xiàn)中,miniblog 針對(duì) HTTP 和 gRPC 請(qǐng)求分別設(shè)計(jì)了對(duì)應(yīng)的校驗(yàn)機(jī)制,其中 HTTP 請(qǐng)求在路由層實(shí)現(xiàn)校驗(yàn),gRPC 請(qǐng)求則通過(guò)攔截器完成參數(shù)驗(yàn)證。
最后,文章通過(guò)接口測(cè)試和請(qǐng)求處理功能測(cè)試驗(yàn)證了參數(shù)校驗(yàn)方案的正確性與可靠性,為 Go 項(xiàng)目開(kāi)發(fā)提供了實(shí)用的參考。