Skip to content

Validator 驗證庫

官方地址:go-playground/validator: 💯Go Struct and Field validation, including Cross Field, Cross Struct, Map, Slice and Array diving (github.com)

文檔地址:validator/README.md at master · go-playground/validator (github.com)

官方示例:validator/_examples at master · go-playground/validator (github.com)

基准測試:go-playground/validator: 💯Go Struct and Field validation, including Cross Field, Cross Struct, Map, Slice and Array diving (github.com)

介紹

go-playground/validator實現了一款基於結構體標簽的值驗證器,它有著以下獨一無二的特性:

  • 可使用驗證標簽和自定義驗證器來進行跨字段和跨結構體驗證

  • 切片,數組,map,或者任何多維域都可以被驗證

  • 可以深入驗證 map 的 key 和 value

  • 在驗證之前,通過其基本類型來確定如何進行處理

  • 可以處理自定義字段類型

  • 支持別名標簽,它將允許多個驗證映射到單個標簽上,以便更容易的定義對於結構體的驗證

  • 可以提取自定義的字段名,例如可以在驗證時提取 JSON 名稱以便在錯誤信息中顯示

  • 自定義多語言錯誤信息

  • gin框架的標准默認驗證組件

安裝

powershell
go get github.com/go-playground/validator/v10

導入

go
import "github.com/go-playground/validator/v10"

標簽

驗證器有著非常多的基礎驗證標簽,所有標簽對應的驗證函數都可以在baked_in.go文件中找到,驗證器的結構體 Tag 是valiadte

例如

go
type User {
  age int `validate:"gte=18"` //表示大於等於18歲
}

也可以通過setTagName方法來修改默認 Tag 。

字段

TagDescription
eqcsfield在一個單獨的結構中,驗證當前字段的值是否等於由 param 的值指定的字段
eqfield驗證當前字段的值是否等於參數值指定的字段
fieldcontains驗證當前字段的值是否包含由參數值指定的字段
fieldexcludes驗證當前字段的值是否不包含由參數值指定的字段
gtcsfield在一個單獨的結構中,驗證當前字段的值是否大於由參數的值指定的字段
gtecsfield在一個單獨的結構中,驗證當前字段的值是否大於或等於由參數的值指定的字段
gtefield驗證當前字段的值是否大於或等於由參數值指定的字段
gtfield驗證當前字段的值是否大於由參數值指定的字段
ltcsfield在一個單獨的結構中,驗證當前字段的值是否小於由參數的值指定的字段
ltecsfield在一個單獨的結構中,驗證當前字段的值是否小於等於由參數的值指定的字段
ltefield驗證當前字段的值是否小於或等於由參數值指定的字段
ltfield驗證當前字段的值是否小於由參數值指定的字段
necsfield驗證當前字段的值不等於由參數的值指定的單獨結構中的字段
nefield驗證當前字段的值是否不等於參數值指定的字段

網絡

TagDescription
cidr無類域間路由 CIDR
cidrv4無類域間路由 CIDRv4
cidrv6無類域間路由 CIDRv6
datauri數據統一資源定位符
fqdn完全限定域名(FQDN)
hostname主機名 RFC 952
hostname_port通常用於套接字地址的字段驗證<dns>:<port>組合
hostname_rfc1123主機名 RFC 952
ip因特網協議地址 IP
ip4_addr因特網協議地址 IPv4
ip6_addr因特網協議地址 IPv6
ip_addr因特網協議地址 IP
ipv4因特網協議地址 IPv4
ipv6因特網協議地址 IPv6
mac媒體存取控制位址,也稱局域網地址
tcp4_addr傳輸控制協議地址 TCP4
tcp6_addr傳輸控制協議地址 TCPv6
tcp_addr傳輸控制協議地址 TCP
udp4_addr用戶數據報協議地址 UDPv4
udp6_addr用戶數據報協議地址 UDPv6
udp_addr用戶數據報協議地址 UDP
unix_addrUnix 域套接字端點地址
uri統一資源標識符
url統一資源定位符
url_encoded統一資源標識符編碼
urn_rfc2141RFC 2141 統一資源名

字符串

TagDescription
alpha驗證當前字段的值是否是有效的字母
alphanum驗證當前字段的值是否是有效的字母數字
alphanumunicode驗證當前字段的值是否是有效的字母數字 unicode 值
alphaunicode驗證當前字段的值是否是有效的字母 unicode 值
ascii驗證字段的值是否為有效的 ASCII 字符
boolean驗證當前字段的值是否為有效的布爾值或是否可以安全地轉換為布爾值
contains驗證字段的值是否包含參數中指定的文本
containsany驗證字段的值是否包含參數中指定的任何字符
containsrune驗證字段的值是否包含參數中指定的符文
endsnotwith驗證字段的值不以參數中指定的文本結束
endswith驗證字段的值以參數中指定的文本結束
excludes驗證字段的值不包含參數中指定的文本
excludesall驗證字段的值不包含參數中指定的任何字符
excludesrune驗證字段的值不包含參數中指定的字符
lowercase驗證當前字段的值是否為小寫字符串
multibyte驗證字段的值是否具有多字節字符
number驗證當前字段的值是否為有效數字
numeric驗證當前字段的值是否是有效的數值
printascii驗證字段的值是否是有效的可打印 ASCII 字符
startsnotwith驗證字段的值不是以參數中指定的文本開始
startswith驗證字段的值是否以參數中指定的文本開始
uppercase驗證當前字段的值是否為大寫字符串

格式化

TagDescription
base64Base64 字符串
base64urlBase64URL 字符串
bic驗證當前字段的值是否為 ISO 9362 中定義的有效的 BIC 碼(SWIFT 代碼)
bcp47_language_tag驗證當前字段的值是否為 BCP47 規范的語言標簽
btc_addr驗證字段的值是否為有效的 BTC 地址
btc_addr_bech32驗證字段的值是否為有效的 bech32 BTC 地址
credit_card驗證當前字段的值是否是有效的信用卡號
datetime驗證當前字段的值是否是有效的時間日期字符串
e164驗證當前字段的值是否為有效的 e.164 格式的電話號碼
email驗證當前字段的值是否是有效的電子郵件地址
eth_addr驗證字段的值是否為有效的以太坊地址
hexadecimal驗證當前字段的值是否為有效的十六進制
hexcolor驗證當前字段的值是否是有效的十六進制顏色
hsl驗證當前字段的值是否是有效的 HSL 顏色
hsla驗證當前字段的值是否是有效的 HSLA 顏色
html驗證當前字段的值是否是有效的 HTML
html_encoded驗證當前字段的值是否是有效的 HTML 編碼
isbn驗證字段的值是否為有效的 v10 或 v13 ISBN(國際標准書號)
isbn10驗證字段的值是否為有效的 v10 ISBN(國際標准書號)
isbn13驗證字段的值是否為有效的 v13 ISBN(國際標准書號)
iso3166_1_alpha2驗證當前字段的值是否為有效的 iso3166-1 alpha-2 國家代碼
iso3166_1_alpha3驗證當前字段的值是否為有效的 iso3166-1 alpha-3 國家代碼
iso3166_1_alpha_numeric驗證當前字段的值是否為有效的 iso3166-1 字母數字國家代碼
iso3166_2驗證當前字段的值是否為有效的國家地區代碼 (ISO 3166-2)
iso4217驗證當前字段的值是否為有效的貨幣代碼 (ISO 4217)
json驗證當前字段的值是否為有效的 json 字符串
jwt驗證當前字段的值是否是有效的 JWT 字符串
latitude驗證字段的值是否是有效的緯度坐標
longitude驗證字段的值是否是有效的緯度坐標
postcode_iso3166_alpha2根據 iso 3166 alpha 2 中國家代碼的值進行驗證
postcode_iso3166_alpha2_field通過字段驗證,該字段表示 iso 3166 alpha 2 中的國家代碼值
rgb驗證當前字段的值是否是有效的 RGB 顏色
rgba驗證當前字段的值是否是有效的 RGBA 顏色
ssn驗證字段的值是否是有效的 SSN
timezone驗證當前字段的值是否是有效的時區字符串
uuid驗證字段的值是否是任何版本的有效 UUID
uuid3驗證字段的值是否是任的有效 UUID v3
uuid3_rfc4122驗證字段的值是否為有效的 RFC4122 v3 UUID
uuid4驗證字段的值是否為有效的 v4 UUID
uuid4_rfc4122驗證字段的值是否為有效的 RFC4122 v4 UUID
uuid5驗證字段的值是否是有效的 v5 UUID
uuid5_rfc4122驗證字段的值是否是有效的 RFC4122 v5 UUID
uuid_rfc4122驗證字段的值是否為任何版本的有效 RFC4122 UUID
md4驗證字段的值是否為有效的 MD4
md5驗證字段的值是否為有效的 MD5
sha256驗證該字段的值是否是有效的 SHA256
sha384驗證字段的值是否是有效的 SHA384
sha512驗證字段的值是否為有效的 SHA512
ripemd128驗證字段的值是否是有效的 PIPEMD128
ripemd128驗證字段的值是否是有效的 PIPEMD160
tiger128驗證字段的值是否是有效的 TIGER128
tiger160驗證字段的值是否是有效的 TIGER160
tiger192驗證字段的值是否是有效的 TIGER192
semver驗證當前字段的值是否為語義版本 2.0.0 中定義的有效 semver 版本
ulid驗證字段的值是否為有效的 ULID

比較

TagDescription
eq等於
gt大於
gte大於等於
lt小於
lte小於等於
ne不等於

其他

TagDescription
dir文件目錄
file文件路徑
isdefault驗證當前字段的值是否為默認靜態值
len字段長度
max最大值
min最小值
oneof是否為列舉值中的一個
oimtempty若字段未設置,則忽略該字段
required必須值
required_if僅當所有其他指定字段與指定值相等時,驗證字段必須存在且不為空
required_unless除非所有其他指定字段與指定值相等,否則驗證字段必須存在且不為空
required_with當指定字段中任意一個存在時,驗證字段必須存在且不為空
required_with_all當指定的所有字段均存在時,驗證字段必須存在且不為空
required_without當指定字段中任意一個不存在時,驗證字段必須存在且不為空
required_without_all當指定的所有字段均不存在時,驗證字段必須存在且不為空
excluded_if僅當所有其他指定字段與指定值相等時,驗證字段可不存在或為空
excluded_unless除非所有其他指定字段與指定值相等,否則驗證字段可不存在或為空
excluded_with當指定字段中任意一個存在時,驗證字段可不存在或為空
excluded_with_all當指定的所有字段均存在時,驗證字段可不存在或為空
excluded_without當指定字段中任意一個不存在時,驗證字段可不存在或為空
excluded_without_all當指定的所有字段均不存在時,驗證字段可不存在或為空
unique驗證每個 arrmapslice 值是否唯一

別名

TagDescription
iscolorhexcolor|rgb|rgba|hsl|hsla
country_codeiso3166_1_alpha2|iso3166_1_alpha3|iso3166_1_alpha_numeric

操作符

| Tag | Description | Hex | | --- | -------------------------------------------------------------------- | -------------------------------------------------- | ------ | | , | 與操作,使用多個驗證標記,必須所有條件都滿足,隔開逗號之間不能有空格 | 0x2c | | | | 或操作,使用多個驗證標記,但是只需滿足其中一個即可 | 0x7c | | - | 該字段跳過驗證 | 0x2d | | = | 參數匹配符號 | 0x3d |

TIP

驗證字段的時候想要匹配操作符的話,需要使用utf8十六進制表達形式替換,例如

go
filed string `validate:"contains=0x2c"`

使用

下面會介紹Validator的一些基本使用以及一些代碼示例。

單例

go
var validate *validator.Validate

在使用時,官方建議在整個程序的生命周期中,只存在一個驗證器實例,這樣會有利於其緩存一些數據。

創建驗證器

在單獨使用Validator沒有集成其他框架的情況下,需要我們手動創建驗證器。

go
validate = validator.New()

結構體驗證

go
func (v *Validate) Struct(s interface{}) error

Struct方法用於驗證一個結構體所有公開的字段,默認會自動進行嵌套結構體驗證,當傳入非法的值或者傳入值為nil時,會返回InvalidValidationError,如果驗證失敗的錯誤則返回ValidationErrors

示例

go
package validate

import (
  "fmt"
  "github.com/go-playground/validator/v10"
  "testing"
)

type User struct {
  Name    string `validate:"contains=jack"` //名字包含jack
  Age     int    `validate:"gte=18"`        //大於等於17歲
  Address string `valiate:"endwith=市"`      //以市結尾
}

func TestStruct(t *testing.T) {
  validate := validator.New()
  user := User{
    Name:    "jacklove",
    Age:     17,
    Address: "滔博市",
  }
  err := validate.Struct(user)
  for _, err := range err.(validator.ValidationErrors) {
    fmt.Println(err.Namespace()) //命名
    fmt.Println(err.Field())
    fmt.Println(err.StructNamespace())
    fmt.Println(err.StructField())
    fmt.Println(err.Tag())
    fmt.Println(err.ActualTag())
    fmt.Println(err.Kind())
    fmt.Println(err.Type())
    fmt.Println(err.Value())
    fmt.Println(err.Param())
    fmt.Println()
  }
  fmt.Println(err)
}

輸出

User.Age
Age
User.Age
Age
gte
gte
int
int
17
18

Key: 'User.Age' Error:Field validation for 'Age' failed on the 'gte' tag

map 驗證

go
func (v *Validate) ValidateMap(data map[string]interface{}, rules map[string]interface{}) map[string]interface{}

通過一個 Tagmap來進行鍵值對驗證。

示例

go
func TestMap(t *testing.T) {
   user := map[string]interface{}{
      "name":    "jak",
      "age":     17,
      "address": "滔博市",
   }
   rules := map[string]interface{}{
      "name":    "contains=jacklove",
      "age":     "gte=18",
      "address": "endswith=市",
   }

   validate := validator.New()

   validateMap := validate.ValidateMap(user, rules)
   fmt.Println(validateMap)
}

輸出

map[age:Key: '' Error:Field validation for '' failed on the 'gte' tag name:Key: '' Error:Field validation for '' failed on the 'contains' tag]

切片驗證

驗證字符串切片,dive前是 tag 是對切片進行驗證,dive後的 tag 是對切片中的值進行驗證,嵌套切片也是一個道理,有幾維就用幾個dive

go
func TestSlice1(t *testing.T) {
  list := []string{"jack", "mike", "lisa", "golang"}
  err := validator.New().Var(list, "max=5,dive,contains=a,min=5") //切片長度最大值為5,元素必須包含字符a,且最小長度為5
  fmt.Println(err)
}

輸出

Key: '[0]' Error:Field validation for '[0]' failed on the 'min' tag
Key: '[1]' Error:Field validation for '[1]' failed on the 'contains' tag
Key: '[2]' Error:Field validation for '[2]' failed on the 'min' tag

對切片裡的每一個用戶進行結構體驗證

go
func TestSlice(t *testing.T) {
   userList := make([]User, 0)
   user := User{
      Name:    "jacklove",
      Age:     17,
      Address: "滔博市",
   }
   userList = append(userList, user)
   err := validator.New().Var(userList, "dive") //「dive」即深層驗證的意思,當元素為結構體時,會自動進行結構體驗證
   fmt.Println(err)
}

輸出

Key: '[0].Age' Error:Field validation for 'Age' failed on the 'gte' tag

變量驗證

比較簡單易懂,就不做過多的解釋

例 1

go
func TestVar(t *testing.T) {
   name := "jack"
   err := validator.New().Var(name, "max=5,contains=a,min=1,endswith=l") //最大長度為5,最小長度為1,包含字母a,以字母l結尾
   fmt.Println(err)
}

輸出

Key: '' Error:Field validation for '' failed on the 'endswith' tag

例 2

func TestVar1(t *testing.T) {
   age := 18
   err := validator.New().Var(age, "gte=19")
   fmt.Println(err)
}

輸出

Key: '' Error:Field validation for '' failed on the 'gte' tag

TIP

Var方法可以驗證的類型包含結構體,變量,切片,map,要合理結合dive標簽使用。

字段驗證

字段驗證的參數不再是基本類型,而是結構體的字段名,可以是自身的字段名,也可以是嵌套結構體的字段名。

go
type Password struct {
   FirstPassword  string `validate:"eqfield=SecondPassword"` //驗證兩次輸入的密碼是否相等
   SecondPassword string
}

type RegisterUser struct {
   Username string `validate:"necsfield=Password.FirstPassword"` //在注冊時為了安全考慮,禁止密碼和用戶名一致
   Password Password
}

func TestCrossStructFieldValidate(t *testing.T) {
   validate = validator.New()
   // 失敗
   fmt.Println(validate.Struct(RegisterUser{
      Username: "gopher",
      Password: Password{
         FirstPassword:  "gopher",
         SecondPassword: "gophers",
      },
   }))
   // 成功
   fmt.Println(validate.Struct(RegisterUser{
      Username: "gophers",
      Password: Password{
         FirstPassword:  "gopher",
         SecondPassword: "gopher",
      },
   }))
}

輸出

Key: 'RegisterUser.Username' Error:Field validation for 'Username' failed on the 'necsfield' tag
Key: 'RegisterUser.Password.FirstPassword' Error:Field validation for 'FirstPassword' failed on the 'eqfield' tag
<nil>

WARNING

使用字段驗證時,當 Tag 作為參數的字段或者結構體不存在時,會直接判斷為驗證失敗,例如:

go
type Password struct {
   FirstPassword  string `validate:"eqfield=SeconddPaswod"` // SeconddPaswod != SecondPassword
   SecondPassword string
}

對於這種拼寫錯誤,很難檢查到,而且驗證時也僅會以驗證未通過的形式展現,需要十分注意。

進階

接下來會講解一些進階的使用技巧與更多的自定義操作。

自定義別名

在有些時候,對於一個字段有非常多的驗證 tag,當你想要復用到另一個字段上時,你可能會直接賦值粘貼,不過這並不是最好的解決辦法,更好的方法是通過注冊別名來提高復用性,請看下面的一個例子:

go
var validate *validator.Validate

const PERSON_NAME_RULES = "max=10,min=1,contains=jack"

func TestAlias(t *testing.T) {
  validate = validator.New()
    // 注冊別名
  validate.RegisterAlias("namerules", PERSON_NAME_RULES)
  type person struct {
    FirstName string `validate:"namerules"` // 使用別名
    LastName  string `validate:"namerules"`
  }

  err := validate.Struct(person{
    FirstName: "",
    LastName:  "",
  })

  fmt.Println(err)
}

輸出

go
Key: 'person.FirstName' Error:Field validation for 'FirstName' failed on the 'namerules' tag
Key: 'person.LastName' Error:Field validation for 'LastName' failed on the 'namerules' tag

自定義驗證函數

雖然組件自帶的驗證 tag 足夠滿足基本時候,可有些時候對於一些特殊需求必須要自己定義邏輯,Validator為我們提供了相關的 API 來自定義驗證函數。接下來先看一個例子:

go
func TestCustomValidate(t *testing.T) {
   validate = validator.New()
   fmt.Println(validate.RegisterValidation("is666", is666))
   type Example struct {
      Name string `validate:"is666"`
   }
   fmt.Println(validate.Struct(Example{Name: "777"}))
   fmt.Println(validate.Struct(Example{Name: "666"}))
}

func is666(fl validator.FieldLevel) bool {
   return fl.Field().String() == "666"
}

創建了一個函數,判斷字段值是不是等於"666",並且其對應的 Tag 是is666,輸出如下

go
<nil>
Key: 'Example.Name' Error:Field validation for 'Name' failed on the 'is666' tag

TIP

注冊的 Tag 如果已經存在,那麼將會被現有的覆蓋掉,也就是說可以「重寫」默認的 Tag 校驗邏輯。

自定義類型驗證函數

類型驗證函數是專門針對某一類型的,通常用於一些非基本類型,同樣的也可以覆蓋默認基本類型的校驗,下面看一個例子:

go
type Address struct {
  name string
}

func TestCustomTypeValidate(t *testing.T) {
  validate = validator.New()
  validate.RegisterCustomTypeFunc(ValidateAddress, Address{}) // 注冊類型驗證函數和對應的類型
  type Example struct {
    Address Address `validate:"required"`
  }
  fmt.Println(validate.Struct(Example{Address: Address{name: ""}}))
  fmt.Println(validate.Struct(Example{Address: Address{name: "cn"}}))
}

func ValidateAddress(value reflect.Value) interface{} {
  if address, ok := value.Interface().(Address); ok {
    //錯誤處理
    if address.name == "" {
      return address.name
    }

    return value //返回字段即代表驗證正確
  }
  return nil
}

輸出

go
Key: 'Example.Address' Error:Field validation for 'Address' failed on the 'required' tag
<nil>

TIP

同時將多個類型注冊到一個函數也是同樣的道理

自定義結構體驗證函數

結構體驗證函數的區別在於,其他函數的參數是字段,而此函數的參數是結構體,看下面的例子:

go
type People struct {
   FirstName string
   LastName  string
}

func TestCustomStructLevel(t *testing.T) {
   validate = validator.New()
   validate.RegisterStructValidation(PeopleValidate, People{}) //同類型注冊,可以傳入的結構體也不止一種
   err := validate.Struct(People{
      FirstName: "",
      LastName:  "",
   })
   fmt.Println(err)
}

func PeopleValidate(sl validator.StructLevel) {
   people := sl.Current().Interface().(People)

   if people.FirstName == "" || people.LastName == "" {
      sl.ReportError(people.FirstName, "FirstName", "FirstName", "", "")
      sl.ReportError(people.FirstName, "LastName", "LastName", "", "")
   }
}

輸出

Key: 'People.FirstName' Error:Field validation for 'FirstName' failed on the '' tag
Key: 'People.LastName' Error:Field validation for 'LastName' failed on the '' tag

多語言

翻譯器組件

go get github.com/go-playground/universal-translator

地區組件

go get github.com/go-playground/locales

驗證器默認的語言是英文,而我們在進行項目開發時,可能會用到不止一種語言,這時候我們就需要用到國際化多語言組件,看下面的一個例子:

go
import (
  "fmt"
  "github.com/go-playground/locales/zh"
  ut "github.com/go-playground/universal-translator"
  "github.com/go-playground/validator/v10"
  zh_trans "github.com/go-playground/validator/v10/translations/zh"
  "reflect"
  "testing"
)

type User struct {
   Name    string `validate:"contains=jack"` //名字包含jack
   Age     int    `validate:"gte=18"`        //大於等於17歲
   Address string `validate:"endswith=市"`    //以市結尾
}

var (
   uni      *ut.UniversalTranslator
   validate *validator.Validate
)

func TestTranslate(t *testing.T) {
   zh := zh.New()
   //第一個是備用的,後續的是支持的語言,可以有多個
   uni = ut.New(zh, zh)
   //這裡的語言通常可以在http的請求頭中的Accept-Language中獲取對應的語言
   trans, found := uni.GetTranslator(zh.Locale())
   validate = validator.New()
   if found {
      zh_trans.RegisterDefaultTranslations(validate, trans) //注冊默認翻譯器
   }
   err := validate.Struct(User{
      Name:    "",
      Age:     0,
      Address: "",
   })
   fmt.Println(err.(validator.ValidationErrors).Translate(trans))
}

輸出

map[User.Address:Address必須以文本'市'結尾 User.Age:Age必須大於或等於18 User.Name:Name必須包含文本'jack']

也可以把每一個錯誤單獨翻譯

go
for _, fieldError := range err.(validator.ValidationErrors) {
   fmt.Println(fieldError.Translate(trans))
}

輸出

Name必須包含文本'jack'
Age必須大於或等於18
Address必須以文本'市'結尾

可以看到返回值是一個 map,可以看到基本的錯誤信息翻譯已經做到了,但是還不足以納入使用,我們需要接著來美化一下錯誤信息,以便更好的與客戶或者前端進行對接。

go
type User struct {

   Name    string `validate:"contains=jack" label:"姓名"` //名字包含jack
   Age     int    `validate:"gte=18" label:"年齡"`        //大於等於17歲
   Address string `validate:"endswith=市" label:"地址"`    //以市結尾
   Sex     string `validate:"required" label:"性別"`
}

首先自定義 Taglabel,它的值就是字段的中文名,隨後通過驗證器注冊一個TagNameFunc,它的作用是在獲取字段名時或替換掉原名稱。在errors.go文件中的Filed() string方法上的注釋如下說道:"帶有標記名的字段名優先於字段的實際名稱",所以後續在發生錯誤時,就可以使用自定義的中文名來替代英文單詞。TagNameFunc如下:

go
// 我們加上了一個自定義標簽,這個標簽用於給結構體字段做中文名,它會替代原本的字段名稱
func CustomTagNameFunc(field reflect.StructField) string {
   label := field.Tag.Get("label")
   if len(label) == 0 {
      return field.Name
   }
   return label
}

最後再注冊

go
validate.RegisterTagNameFunc(CustomTagNameFunc)

再次執行輸出

姓名必須包含文本'jack'
年齡必須大於或等於18
地址必須以文本'市'結尾

然後這還不夠,依舊不足以作為錯誤信息返回給前端,我們需要將信息格式化成 json 或者任何適合消息傳輸的格式,你可能會想到直接將 map 序列化成 json,這是一種解決辦法,不過你可能會得到如下結果:

json
{
  "User.地址": "地址必須以文本'市'結尾",
  "User.姓名": "姓名必須包含文本'back'",
  "User.年齡": "年齡必須大於或等於18",
  "User.性別": "性別為必填字段"
}

通過將 map 的 key 值處理下得到如下結果:

json
{
  "地址": "地址必須以文本'市'結尾",
  "姓名": "姓名必須包含文本'back'",
  "年齡": "年齡必須大於或等於18",
  "性別": "性別為必填字段"
}

不過並不建議將上面這種的信息返回給前端,我們可以處理成一個字符串作為信息返回

姓名必須包含文本'back', 年齡必須大於或等於18, 地址必須以文本'市'結尾, 性別為必填字段,

完整代碼

go
import (
   "fmt"
   "github.com/go-playground/locales/zh"
   ut "github.com/go-playground/universal-translator"
   "github.com/go-playground/validator/v10"
   zh_trans "github.com/go-playground/validator/v10/translations/zh"
   "reflect"
   "strings"
   "testing"
)

type User struct {
   Name    string `validate:"contains=back" label:"姓名"` //名字包含jack
   Age     int    `validate:"gte=18" label:"年齡"`        //大於等於17歲
   Address string `validate:"endswith=市" label:"地址"`    //以市結尾
   Sex     string `validate:"required" label:"性別"`
}

var (
   uni      *ut.UniversalTranslator
   validate *validator.Validate
)

// 我們加上了一個自定義標簽,這個標簽用於給結構體字段做中文名,它會替代原本的字段名稱
func CustomTagNameFunc(field reflect.StructField) string {
   label := field.Tag.Get("label")
   if len(label) == 0 {
      return field.Name
   }
   return label
}

func TestTranslate(t *testing.T) {
   zh := zh.New()
   uni = ut.New(zh, zh)
   //這裡的語言通常可以在http的請求頭中的Accept-Language中獲取對應的語言
   trans, found := uni.GetTranslator(zh.Locale())
   validate = validator.New()
   if found {
      zh_trans.RegisterDefaultTranslations(validate, trans) //注冊默認翻譯器
   }
   validate.RegisterTagNameFunc(CustomTagNameFunc)
   err := validate.Struct(User{
      Name:    "",
      Age:     0,
      Address: "",
   })
   translate := errInfoFormat(err.(validator.ValidationErrors), trans)
   fmt.Println(translate)
}

func errInfoFormat(errors validator.ValidationErrors, trans ut.Translator) string {
   builder := strings.Builder{}
   for _, err := range errors {
      builder.WriteString(err.Translate(trans))
      builder.WriteString(", ")
   }
   return builder.String()
}

最後的最後,如果覺得錯誤信息太冰冷,希望更人性化一點,可以重寫指定 tag 的錯誤提示信息,這需要用到RegisterTranslation方法,同時也需要用到兩個類型的函數,分別是RegisterTranslationsFunc負責注冊對應 Tag 的翻譯模板,另一個則是TranslationFunc,負責將模板處理得到最後的翻譯內容。這裡用required舉個例子:

go
func requiredOverrideRegister(ut ut.Translator) error { //這個函數的作用是注冊翻譯模板
  return ut.Add("required", "{}是一個必須填寫的字段", true) // {}是佔位符 true代表了是否重寫已有的模板
}

func requiredOverrideTranslation(ut ut.Translator, fe validator.FieldError) string { // 這個函數的作用是負責翻譯內容
  t, _ := ut.T("required", fe.Field()) //參數可以有多個,取決於注冊對應Tag的模板有多少個佔位符
  return t
}

最後注冊一下

go
validate.RegisterTranslation("required", trans, requiredOverrideRegister, requiredOverrideTranslation)

結果

姓名必須包含文本'back', 年齡必須大於或等於18, 地址必須以文本'市'結尾, 性別是一個必須填寫的字段,

語言文件

事實上一個個寫代碼注冊非常的繁瑣,universal-translator提供了通過編寫JSON 配置文件的方式來進行翻譯:universal-translator/examples/full-with-files at master · go-playground/universal-translator (github.com)

go
func TestFilei18n(t *testing.T) {
   validate = validator.New()
   zh := zh.New()
   universalTranslator := ut.New(zh, zh)
   translator, _ := universalTranslator.GetTranslator(zh.Locale())
   zh_trans.RegisterDefaultTranslations(validate, translator)
   er := universalTranslator.Import(ut.FormatJSON, "./zh.json") //建議要在注冊之後導入,這樣才能覆蓋原有的Tag
   if er != nil {
      log.Fatal(er)
   }
   type Gopher struct {
      Language string `validate:"required"`
   }

   err := validate.Struct(Gopher{
      "",
   })
   fmt.Println(err.(validator.ValidationErrors).Translate(translator))
}

JSON 文件

json
[
  {
    "locale": "zh",
    "key": "required",
    "trans": "這是一個十分重要的字段{0},你必須填寫它",
    "override": true
  }
]

輸出

map[Gopher.Language:這是一個十分重要的字段Language,你必須填寫它]

TIP

universal-translator在使用時有很多坑,如果是想要覆蓋原有的Tag的話,typerule都可以不填,因為原有的配置中也沒有填,最好保持一致。填了什麼type,就會將配置加到對應的 map 中,如果是Cardinal或者其他的typerule配置了one之類的,那麼就需要本地做相應的配置才能正常使用,否則將會報錯。

Golang學習網由www.golangdev.cn整理維護