偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

Go 優(yōu)秀實(shí)踐:請(qǐng)求參數(shù)校驗(yàn)方法設(shè)計(jì)和實(shí)現(xiàn)

開(kāi)發(fā) 后端
本文詳細(xì)介紹了在 Go 項(xiàng)目開(kāi)發(fā)中如何實(shí)現(xiàn) API 接口請(qǐng)求參數(shù)的校驗(yàn)邏輯,并以 miniblog 項(xiàng)目為例進(jìn)行了實(shí)踐。

本節(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í)用的參考。

責(zé)任編輯:趙寧寧 來(lái)源: 令飛編程
相關(guān)推薦

2010-03-17 14:50:50

Fedora Core

2025-05-20 08:20:00

GoGo Context上下文

2022-10-30 23:13:30

contextGo語(yǔ)言

2023-06-08 16:47:09

軟件開(kāi)發(fā)工具

2020-04-22 09:00:00

REST API參數(shù)化前端

2023-10-30 16:14:44

Metrics SD數(shù)據(jù)庫(kù)

2023-10-26 11:07:48

Golang開(kāi)發(fā)

2021-12-15 09:51:42

Web開(kāi)發(fā)數(shù)據(jù)

2024-01-11 11:25:22

2023-10-27 12:11:33

2023-05-16 15:25:08

2022-10-20 10:02:16

前端測(cè)試開(kāi)發(fā)

2022-04-20 12:08:17

容器安全漏洞網(wǎng)絡(luò)安全

2019-10-10 09:00:30

云端云遷移云計(jì)算

2022-11-28 23:48:06

JavaScript編程語(yǔ)言技巧

2022-11-23 10:49:41

IT資產(chǎn)管理IT戰(zhàn)略

2023-02-23 15:56:51

2024-10-29 20:58:38

2023-06-16 08:36:25

多線程編程數(shù)據(jù)競(jìng)爭(zhēng)

2022-12-19 07:28:53

Kubernetes資源請(qǐng)求限制
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)