Validator 驗證庫
文檔地址:validator/README.md at master · go-playground/validator (github.com)
官方示例:validator/_examples at master · go-playground/validator (github.com)
介紹
go-playground/validator實現了一款基於結構體標簽的值驗證器,它有著以下獨一無二的特性:
可使用驗證標簽和自定義驗證器來進行跨字段和跨結構體驗證
切片,數組,map,或者任何多維域都可以被驗證
可以深入驗證 map 的 key 和 value
在驗證之前,通過其基本類型來確定如何進行處理
可以處理自定義字段類型
支持別名標簽,它將允許多個驗證映射到單個標簽上,以便更容易的定義對於結構體的驗證
可以提取自定義的字段名,例如可以在驗證時提取 JSON 名稱以便在錯誤信息中顯示
自定義多語言錯誤信息
gin框架的標准默認驗證組件
安裝
go get github.com/go-playground/validator/v10導入
import "github.com/go-playground/validator/v10"標簽
驗證器有著非常多的基礎驗證標簽,所有標簽對應的驗證函數都可以在baked_in.go文件中找到,驗證器的結構體 Tag 是valiadte,
例如
type User {
age int `validate:"gte=18"` //表示大於等於18歲
}也可以通過setTagName方法來修改默認 Tag 。
字段
| Tag | Description |
|---|---|
eqcsfield | 在一個單獨的結構中,驗證當前字段的值是否等於由 param 的值指定的字段 |
eqfield | 驗證當前字段的值是否等於參數值指定的字段 |
fieldcontains | 驗證當前字段的值是否包含由參數值指定的字段 |
fieldexcludes | 驗證當前字段的值是否不包含由參數值指定的字段 |
gtcsfield | 在一個單獨的結構中,驗證當前字段的值是否大於由參數的值指定的字段 |
gtecsfield | 在一個單獨的結構中,驗證當前字段的值是否大於或等於由參數的值指定的字段 |
gtefield | 驗證當前字段的值是否大於或等於由參數值指定的字段 |
gtfield | 驗證當前字段的值是否大於由參數值指定的字段 |
ltcsfield | 在一個單獨的結構中,驗證當前字段的值是否小於由參數的值指定的字段 |
ltecsfield | 在一個單獨的結構中,驗證當前字段的值是否小於等於由參數的值指定的字段 |
ltefield | 驗證當前字段的值是否小於或等於由參數值指定的字段 |
ltfield | 驗證當前字段的值是否小於由參數值指定的字段 |
necsfield | 驗證當前字段的值不等於由參數的值指定的單獨結構中的字段 |
nefield | 驗證當前字段的值是否不等於參數值指定的字段 |
網絡
| Tag | Description |
|---|---|
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_addr | Unix 域套接字端點地址 |
uri | 統一資源標識符 |
url | 統一資源定位符 |
url_encoded | 統一資源標識符編碼 |
urn_rfc2141 | RFC 2141 統一資源名 |
字符串
| Tag | Description |
|---|---|
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 | 驗證當前字段的值是否為大寫字符串 |
格式化
| Tag | Description |
|---|---|
base64 | Base64 字符串 |
base64url | Base64URL 字符串 |
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 |
比較
| Tag | Description |
|---|---|
eq | 等於 |
gt | 大於 |
gte | 大於等於 |
lt | 小於 |
lte | 小於等於 |
ne | 不等於 |
其他
| Tag | Description |
|---|---|
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 | 驗證每個 arr、map、slice 值是否唯一 |
別名
| Tag | Description |
|---|---|
iscolor | hexcolor|rgb|rgba|hsl|hsla |
country_code | iso3166_1_alpha2|iso3166_1_alpha3|iso3166_1_alpha_numeric |
操作符
| Tag | Description | Hex | | --- | -------------------------------------------------------------------- | -------------------------------------------------- | ------ | | , | 與操作,使用多個驗證標記,必須所有條件都滿足,隔開逗號之間不能有空格 | 0x2c | | | | 或操作,使用多個驗證標記,但是只需滿足其中一個即可 | 0x7c | | - | 該字段跳過驗證 | 0x2d | | = | 參數匹配符號 | 0x3d |
TIP
驗證字段的時候想要匹配操作符的話,需要使用utf8十六進制表達形式替換,例如
filed string `validate:"contains=0x2c"`使用
下面會介紹Validator的一些基本使用以及一些代碼示例。
單例
var validate *validator.Validate在使用時,官方建議在整個程序的生命周期中,只存在一個驗證器實例,這樣會有利於其緩存一些數據。
創建驗證器
在單獨使用Validator沒有集成其他框架的情況下,需要我們手動創建驗證器。
validate = validator.New()結構體驗證
func (v *Validate) Struct(s interface{}) errorStruct方法用於驗證一個結構體所有公開的字段,默認會自動進行嵌套結構體驗證,當傳入非法的值或者傳入值為nil時,會返回InvalidValidationError,如果驗證失敗的錯誤則返回ValidationErrors 。
示例
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' tagmap 驗證
func (v *Validate) ValidateMap(data map[string]interface{}, rules map[string]interface{}) map[string]interface{}通過一個 Tagmap來進行鍵值對驗證。
示例
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
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對切片裡的每一個用戶進行結構體驗證
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
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' tagTIP
Var方法可以驗證的類型包含結構體,變量,切片,map,要合理結合dive標簽使用。
字段驗證
字段驗證的參數不再是基本類型,而是結構體的字段名,可以是自身的字段名,也可以是嵌套結構體的字段名。
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 作為參數的字段或者結構體不存在時,會直接判斷為驗證失敗,例如:
type Password struct {
FirstPassword string `validate:"eqfield=SeconddPaswod"` // SeconddPaswod != SecondPassword
SecondPassword string
}對於這種拼寫錯誤,很難檢查到,而且驗證時也僅會以驗證未通過的形式展現,需要十分注意。
進階
接下來會講解一些進階的使用技巧與更多的自定義操作。
自定義別名
在有些時候,對於一個字段有非常多的驗證 tag,當你想要復用到另一個字段上時,你可能會直接賦值粘貼,不過這並不是最好的解決辦法,更好的方法是通過注冊別名來提高復用性,請看下面的一個例子:
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)
}輸出
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 來自定義驗證函數。接下來先看一個例子:
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,輸出如下
<nil>
Key: 'Example.Name' Error:Field validation for 'Name' failed on the 'is666' tagTIP
注冊的 Tag 如果已經存在,那麼將會被現有的覆蓋掉,也就是說可以「重寫」默認的 Tag 校驗邏輯。
自定義類型驗證函數
類型驗證函數是專門針對某一類型的,通常用於一些非基本類型,同樣的也可以覆蓋默認基本類型的校驗,下面看一個例子:
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
}輸出
Key: 'Example.Address' Error:Field validation for 'Address' failed on the 'required' tag
<nil>TIP
同時將多個類型注冊到一個函數也是同樣的道理
自定義結構體驗證函數
結構體驗證函數的區別在於,其他函數的參數是字段,而此函數的參數是結構體,看下面的例子:
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驗證器默認的語言是英文,而我們在進行項目開發時,可能會用到不止一種語言,這時候我們就需要用到國際化多語言組件,看下面的一個例子:
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']也可以把每一個錯誤單獨翻譯
for _, fieldError := range err.(validator.ValidationErrors) {
fmt.Println(fieldError.Translate(trans))
}輸出
Name必須包含文本'jack'
Age必須大於或等於18
Address必須以文本'市'結尾可以看到返回值是一個 map,可以看到基本的錯誤信息翻譯已經做到了,但是還不足以納入使用,我們需要接著來美化一下錯誤信息,以便更好的與客戶或者前端進行對接。
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如下:
// 我們加上了一個自定義標簽,這個標簽用於給結構體字段做中文名,它會替代原本的字段名稱
func CustomTagNameFunc(field reflect.StructField) string {
label := field.Tag.Get("label")
if len(label) == 0 {
return field.Name
}
return label
}最後再注冊
validate.RegisterTagNameFunc(CustomTagNameFunc)再次執行輸出
姓名必須包含文本'jack'
年齡必須大於或等於18
地址必須以文本'市'結尾然後這還不夠,依舊不足以作為錯誤信息返回給前端,我們需要將信息格式化成 json 或者任何適合消息傳輸的格式,你可能會想到直接將 map 序列化成 json,這是一種解決辦法,不過你可能會得到如下結果:
{
"User.地址": "地址必須以文本'市'結尾",
"User.姓名": "姓名必須包含文本'back'",
"User.年齡": "年齡必須大於或等於18",
"User.性別": "性別為必填字段"
}通過將 map 的 key 值處理下得到如下結果:
{
"地址": "地址必須以文本'市'結尾",
"姓名": "姓名必須包含文本'back'",
"年齡": "年齡必須大於或等於18",
"性別": "性別為必填字段"
}不過並不建議將上面這種的信息返回給前端,我們可以處理成一個字符串作為信息返回
姓名必須包含文本'back', 年齡必須大於或等於18, 地址必須以文本'市'結尾, 性別為必填字段,完整代碼
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舉個例子:
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
}最後注冊一下
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)
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 文件
[
{
"locale": "zh",
"key": "required",
"trans": "這是一個十分重要的字段{0},你必須填寫它",
"override": true
}
]輸出
map[Gopher.Language:這是一個十分重要的字段Language,你必須填寫它]TIP
universal-translator在使用時有很多坑,如果是想要覆蓋原有的Tag的話,type與rule都可以不填,因為原有的配置中也沒有填,最好保持一致。填了什麼type,就會將配置加到對應的 map 中,如果是Cardinal或者其他的type且rule配置了one之類的,那麼就需要本地做相應的配置才能正常使用,否則將會報錯。
