如何在Go語言中實現表單驗證?整一個validator吧!

左诗右码發表於2024-11-20

在現代 Web 開發中,表單驗證和錯誤處理是至關重要的環節,尤其是在多語言環境下。

本文將透過一個實際的示例,演示如何使用 Go 語言的 Gin 框架結合 validator 包,實現高階的表單驗證功能,並且支援國際化(i18n)的錯誤資訊提示。

背景與需求

假設我們正在開發一個使用者註冊功能,需要對使用者提交的資訊進行嚴格的驗證。例如,使用者名稱不能為空、郵箱格式必須正確、密碼和確認密碼必須一致、使用者年齡應在合理範圍內(如 1 到 130 歲),並且日期欄位不能早於當前日期。除此之外,系統還需要根據使用者的語言偏好提供相應語言的錯誤提示資訊。

程式碼示例

我們將從以下幾個方面展開:

  1. 表單資料的結構定義
  2. 表單驗證器的初始化與自定義
  3. 多語言支援的實現
  4. 處理表單提交與錯誤返回
package main

import (
    "fmt"
    "net/http"
    "reflect"
    "strings"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/gin-gonic/gin/binding"
    "github.com/go-playground/locales/en"
    "github.com/go-playground/locales/zh"
    ut "github.com/go-playground/universal-translator"
    "github.com/go-playground/validator/v10"
    enTranslations "github.com/go-playground/validator/v10/translations/en"
    zhTranslations "github.com/go-playground/validator/v10/translations/zh"
)

// 定義一個全域性翻譯器
var trans ut.Translator

表單資料結構定義

首先,我們定義使用者提交的表單資料結構 SignUpParam。這個結構體中包含了使用者註冊時所需的各個欄位,並透過結構標籤(tags)指定了驗證規則。

type SignUpParam struct {
    Age        uint8  `json:"age" binding:"gte=1,lte=130"`
    Name       string `json:"name" binding:"required"`
    Email      string `json:"email" binding:"required,email"`
    Password   string `json:"password" binding:"required"`
    RePassword string `json:"re_password" binding:"required,eqfield=Password"`
    Date       string `json:"date" binding:"required,datetime=2006-01-02,checkDate"`
}
  • Age 欄位必須在 1 到 130 歲之間。
  • Name 欄位不能為空。
  • Email 欄位必須是有效的電子郵件地址。
  • PasswordRePassword 欄位必須一致。
  • Date 欄位需要使用自定義校驗方法 checkDate,確保輸入日期晚於當前日期。

初始化與自定義表單驗證器

在 Gin 框架中,我們可以透過 binding.Validator.Engine() 獲取到內建的驗證器,並對其進行自定義。

在下面的程式碼中,我們完成了翻譯器的初始化,並註冊了自定義的標籤名稱和驗證方法。

func InitTrans(locale string) (err error) {
    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {

        // 註冊獲取 JSON tag 的自定義方法
        v.RegisterTagNameFunc(func(fld reflect.StructField) string {
            name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
            if name == "-" {
                return ""
            }
            return name
        })

        // 註冊結構體級別的驗證函式
        v.RegisterStructValidation(SignUpParamStructLevelValidation, SignUpParam{})

        // 註冊自定義校驗方法
        if err := v.RegisterValidation("checkDate", customFunc); err != nil {
            return err
        }

        // 初始化多語言支援
        zhT := zh.New() 
        enT := en.New()
        uni := ut.New(enT, zhT, enT)

                var ok bool
        trans, ok = uni.GetTranslator(locale)
        if !ok {
            return fmt.Errorf("uni.GetTranslator(%s) failed", locale)
        }

        // 註冊語言翻譯
        switch locale {
        case "en":
            err = enTranslations.RegisterDefaultTranslations(v, trans)
        case "zh":
            err = zhTranslations.RegisterDefaultTranslations(v, trans)
        default:
            err = enTranslations.RegisterDefaultTranslations(v, trans)
        }
        if err != nil {
            return err
        }

        // 註冊自定義翻譯
        if err := v.RegisterTranslation(
            "checkDate",
            trans,
            registerTranslator("checkDate", "{0}必須晚於當前日期"),
            translate,
        ); err != nil {
            return err
        }
        return
    }
    return
}

實現自定義校驗邏輯

在上面的程式碼中,我們自定義了兩個校驗函式:

  1. customFunc:用於校驗日期是否晚於當前日期。
  2. SignUpParamStructLevelValidation:用於校驗兩個密碼欄位是否一致。
func customFunc(fl validator.FieldLevel) bool {
    date, err := time.Parse("2006-01-02", fl.Field().String())
    if err != nil {
        return false
    }
    return date.After(time.Now())
}

func SignUpParamStructLevelValidation(sl validator.StructLevel) {
    su := sl.Current().Interface().(SignUpParam)
    if su.Password != su.RePassword {
        sl.ReportError(su.RePassword, "re_password", "RePassword", "eqfield", "password")
    }
}

處理多語言錯誤提示

為了確保錯誤資訊能夠根據使用者的語言偏好正確返回,我們註冊了一個自定義的翻譯函式 registerTranslator,並在驗證失敗時使用該函式對錯誤資訊進行翻譯。

// registerTranslator 為自定義欄位新增翻譯功能
func registerTranslator(tag string, msg string) validator.RegisterTranslationsFunc {
    return func(trans ut.Translator) error {
        if err := trans.Add(tag, msg, false); err != nil {
            return err
        }
        return nil
    }
}

// translate 自定義欄位的翻譯方法
func translate(trans ut.Translator, fe validator.FieldError) string {
    msg, err := trans.T(fe.Tag(), fe.Field())
    if err != nil {
        panic(fe.(error).Error())
    }
    return msg
}

主程式邏輯

最後,我們在 Gin 中處理使用者的註冊請求。當使用者提交的資料驗證失敗時,系統會自動返回翻譯後的錯誤提示資訊。

// removeTopStruct 去除欄位名中的結構體名稱標識
// refer from:https://github.com/go-playground/validator/issues/633#issuecomment-654382345
func removeTopStruct(fields map[string]string) map[string]string {
    res := map[string]string{}
    for field, err := range fields {
        res[field[strings.Index(field, ".")+1:]] = err
    }
    return res
}

func main() {
    // 初始化翻譯器
    if err := InitTrans("zh"); err != nil {
        fmt.Printf("初始化翻譯器失敗: %v\n", err)
        return
    }

    r := gin.Default()

    r.POST("/signup", func(c *gin.Context) {
        var u SignUpParam
        if err := c.ShouldBind(&u); err != nil {
            errs, ok := err.(validator.ValidationErrors)
            if !ok {
                c.JSON(http.StatusOK, gin.H{"msg": err.Error()})
                return
            }
            c.JSON(http.StatusOK, gin.H{"msg": removeTopStruct(errs.Translate(trans))})
            return
        }

        // 其他的一些業務邏輯操作……

        c.JSON(http.StatusOK, gin.H{"msg": "success"})
    })

    err := r.Run(":8080")
    if err != nil {
        fmt.Printf("伺服器執行失敗: %v\n", err)
    }
}

總結

本文透過一個完整的示例,展示瞭如何在 Go 語言中使用 Gin 框架實現多語言的表單驗證。

我們不僅探討了基礎的驗證規則,還介紹瞭如何自定義驗證邏輯以及如何實現國際化的錯誤提示。這種方式使得我們的應用程式不僅在功能上更加強大,同時也能更好地適應全球化的需求。

相關文章