Skip to content

Validator Thư viện Xác thực

Địa chỉ chính thức: go-playground/validator: 💯Go Struct and Field validation, including Cross Field, Cross Struct, Map, Slice and Array diving (github.com)

Địa chỉ tài liệu: validator/README.md at master · go-playground/validator (github.com)

Ví dụ chính thức: validator/_examples at master · go-playground/validator (github.com)

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

Giới thiệu

go-playground/validator triển khai một trình xác thực giá trị dựa trên tag struct, nó có các tính năng độc đáo sau:

  • Có thể sử dụng tag xác thực và trình xác thực tùy chỉnh để xác thực cross-field và cross-struct

  • Slice, array, map, hoặc bất kỳ trường đa chiều nào đều có thể được xác thực

  • Có thể xác thực sâu key và value của map

  • Xác định cách xử lý trước khi xác thực thông qua kiểu cơ bản của nó

  • Có thể xử lý các kiểu trường tùy chỉnh

  • Hỗ trợ tag alias, cho phép ánh xạ nhiều trình xác thực vào một tag duy nhất để dễ dàng định nghĩa xác thực cho struct

  • Có thể trích xuất tên trường tùy chỉnh, ví dụ có thể trích xuất tên JSON khi xác thực để hiển thị trong thông báo lỗi

  • Thông báo lỗi đa ngôn ngữ tùy chỉnh

  • Thành phần xác thực tiêu chuẩn mặc định của framework gin

Cài đặt

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

Import

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

Tag

Validator có rất nhiều tag xác thực cơ bản, tất cả các hàm xác thực tương ứng với tag đều có thể được tìm thấy trong file baked_in.go, tag struct của validator là validate,

ví dụ

go
type User {
  age int `validate:"gte=18"` // biểu thị lớn hơn hoặc bằng 18 tuổi
}

Cũng có thể sửa tag mặc định thông qua method setTagName.

Field

TagMô tả
eqcsfieldTrong một struct riêng biệt, xác thực giá trị trường hiện tại bằng với trường được chỉ định bởi giá trị param
eqfieldXác thực giá trị trường hiện tại bằng với trường được chỉ định bởi giá trị param
fieldcontainsXác thực giá trị trường hiện tại chứa trường được chỉ định bởi giá trị param
fieldexcludesXác thực giá trị trường hiện tại không chứa trường được chỉ định bởi giá trị param
gtcsfieldTrong một struct riêng biệt, xác thực giá trị trường hiện tại lớn hơn trường được chỉ định bởi giá trị param
gtecsfieldTrong một struct riêng biệt, xác thực giá trị trường hiện tại lớn hơn hoặc bằng trường được chỉ định bởi giá trị param
gtefieldXác thực giá trị trường hiện tại lớn hơn hoặc bằng trường được chỉ định bởi giá trị param
gtfieldXác thực giá trị trường hiện tại lớn hơn trường được chỉ định bởi giá trị param
ltcsfieldTrong một struct riêng biệt, xác thực giá trị trường hiện tại nhỏ hơn trường được chỉ định bởi giá trị param
ltecsfieldTrong một struct riêng biệt, xác thực giá trị trường hiện tại nhỏ hơn hoặc bằng trường được chỉ định bởi giá trị param
ltefieldXác thực giá trị trường hiện tại nhỏ hơn hoặc bằng trường được chỉ định bởi giá trị param
ltfieldXác thực giá trị trường hiện tại nhỏ hơn trường được chỉ định bởi giá trị param
necsfieldXác thực giá trị trường hiện tại không bằng trường trong struct riêng biệt được chỉ định bởi giá trị param
nefieldXác thực giá trị trường hiện tại không bằng trường được chỉ định bởi giá trị param

Network

TagMô tả
cidrClassless Inter-Domain Routing CIDR
cidrv4Classless Inter-Domain Routing CIDRv4
cidrv6Classless Inter-Domain Routing CIDRv6
datauriData Uniform Resource Locator
fqdnFully Qualified Domain Name (FQDN)
hostnameHostname RFC 952
hostname_portXác thực trường thường được sử dụng cho địa chỉ socket <dns>:<port>
hostname_rfc1123Hostname RFC 952
ipInternet Protocol Address IP
ip4_addrInternet Protocol Address IPv4
ip6_addrInternet Protocol Address IPv6
ip_addrInternet Protocol Address IP
ipv4Internet Protocol Address IPv4
ipv6Internet Protocol Address IPv6
macMedia Access Control Address, còn gọi là địa chỉ LAN
tcp4_addrTransmission Control Protocol Address TCP4
tcp6_addrTransmission Control Protocol Address TCPv6
tcp_addrTransmission Control Protocol Address TCP
udp4_addrUser Datagram Protocol Address UDPv4
udp6_addrUser Datagram Protocol Address UDPv6
udp_addrUser Datagram Protocol Address UDP
unix_addrUnix Domain Socket Endpoint Address
uriUniform Resource Identifier
urlUniform Resource Locator
url_encodedUniform Resource Identifier Encoding
urn_rfc2141RFC 2141 Uniform Resource Name

String

TagMô tả
alphaXác thực giá trị trường hiện tại có phải là chữ cái hợp lệ không
alphanumXác thực giá trị trường hiện tại có phải là chữ số hợp lệ không
alphanumunicodeXác thực giá trị trường hiện tại có phải là chữ số unicode hợp lệ không
alphaunicodeXác thực giá trị trường hiện tại có phải là chữ cái unicode hợp lệ không
asciiXác thực giá trị trường có phải là ký tự ASCII hợp lệ không
booleanXác thực giá trị trường hiện tại có phải là giá trị boolean hợp lệ hoặc có thể chuyển đổi an toàn sang boolean không
containsXác thực giá trị trường có chứa văn bản được chỉ định trong param không
containsanyXác thực giá trị trường có chứa bất kỳ ký tự nào được chỉ định trong param không
containsruneXác thực giá trị trường có chứa rune được chỉ định trong param không
endsnotwithXác thực giá trị trường không kết thúc bằng văn bản được chỉ định trong param
endswithXác thực giá trị trường kết thúc bằng văn bản được chỉ định trong param
excludesXác thực giá trị trường không chứa văn bản được chỉ định trong param
excludesallXác thực giá trị trường không chứa bất kỳ ký tự nào được chỉ định trong param
excludesruneXác thực giá trị trường không chứa rune được chỉ định trong param
lowercaseXác thực giá trị trường hiện tại có phải là string chữ thường không
multibyteXác thực giá trị trường có ký tự multi-byte không
numberXác thực giá trị trường hiện tại có phải là số hợp lệ không
numericXác thực giá trị trường hiện tại có phải là số hợp lệ không
printasciiXác thực giá trị trường có phải là ký tự ASCII có thể in hợp lệ không
startsnotwithXác thực giá trị trường không bắt đầu bằng văn bản được chỉ định trong param
startswithXác thực giá trị trường bắt đầu bằng văn bản được chỉ định trong param
uppercaseXác thực giá trị trường hiện tại có phải là string chữ hoa không

Format

TagMô tả
base64Base64 String
base64urlBase64URL String
bicXác thực giá trị trường hiện tại có phải là mã BIC hợp lệ (mã SWIFT) theo định nghĩa ISO 9362 không
bcp47_language_tagXác thực giá trị trường hiện tại có phải là tag ngôn ngữ theo tiêu chuẩn BCP47 không
btc_addrXác thực giá trị trường có phải là địa chỉ BTC hợp lệ không
btc_addr_bech32Xác thực giá trị trường có phải là địa chỉ bech32 BTC hợp lệ không
credit_cardXác thực giá trị trường hiện tại có phải là số thẻ tín dụng hợp lệ không
datetimeXác thực giá trị trường hiện tại có phải là string ngày giờ hợp lệ không
e164Xác thực giá trị trường hiện tại có phải là số điện thoại định dạng e.164 hợp lệ không
emailXác thực giá trị trường hiện tại có phải là địa chỉ email hợp lệ không
eth_addrXác thực giá trị trường có phải là địa chỉ Ethereum hợp lệ không
hexadecimalXác thực giá trị trường hiện tại có phải là hexadecimal hợp lệ không
hexcolorXác thực giá trị trường hiện tại có phải là màu hexadecimal hợp lệ không
hslXác thực giá trị trường hiện tại có phải là màu HSL hợp lệ không
hslaXác thực giá trị trường hiện tại có phải là màu HSLA hợp lệ không
htmlXác thực giá trị trường hiện tại có phải là HTML hợp lệ không
html_encodedXác thực giá trị trường hiện tại có phải là HTML encoding hợp lệ không
isbnXác thực giá trị trường có phải là ISBN v10 hoặc v13 hợp lệ (Số sách tiêu chuẩn quốc tế) không
isbn10Xác thực giá trị trường có phải là ISBN v10 hợp lệ (Số sách tiêu chuẩn quốc tế) không
isbn13Xác thực giá trị trường có phải là ISBN v13 hợp lệ (Số sách tiêu chuẩn quốc tế) không
iso3166_1_alpha2Xác thực giá trị trường hiện tại có phải là mã quốc gia iso3166-1 alpha-2 hợp lệ không
iso3166_1_alpha3Xác thực giá trị trường hiện tại có phải là mã quốc gia iso3166-1 alpha-3 hợp lệ không
iso3166_1_alpha_numericXác thực giá trị trường hiện tại có phải là mã quốc gia chữ số iso3166-1 hợp lệ không
iso3166_2Xác thực giá trị trường hiện tại có phải là mã vùng quốc gia hợp lệ (ISO 3166-2) không
iso4217Xác thực giá trị trường hiện tại có phải là mã tiền tệ hợp lệ (ISO 4217) không
jsonXác thực giá trị trường hiện tại có phải là string json hợp lệ không
jwtXác thực giá trị trường hiện tại có phải là string JWT hợp lệ không
latitudeXác thực giá trị trường có phải là tọa độ vĩ độ hợp lệ không
longitudeXác thực giá trị trường có phải là tọa độ kinh độ hợp lệ không
postcode_iso3166_alpha2Xác thực theo giá trị mã quốc gia trong iso 3166 alpha 2
postcode_iso3166_alpha2_fieldXác thực qua trường, trường này biểu thị giá trị mã quốc gia trong iso 3166 alpha 2
rgbXác thực giá trị trường hiện tại có phải là màu RGB hợp lệ không
rgbaXác thực giá trị trường hiện tại có phải là màu RGBA hợp lệ không
ssnXác thực giá trị trường có phải là SSN hợp lệ không
timezoneXác thực giá trị trường hiện tại có phải là string múi giờ hợp lệ không
uuidXác thực giá trị trường có phải là UUID hợp lệ bất kỳ phiên bản nào không
uuid3Xác thực giá trị trường có phải là UUID v3 hợp lệ không
uuid3_rfc4122Xác thực giá trị trường có phải là UUID v3 RFC4122 hợp lệ không
uuid4Xác thực giá trị trường có phải là UUID v4 hợp lệ không
uuid4_rfc4122Xác thực giá trị trường có phải là UUID v4 RFC4122 hợp lệ không
uuid5Xác thực giá trị trường có phải là UUID v5 hợp lệ không
uuid5_rfc4122Xác thực giá trị trường có phải là UUID v5 RFC4122 hợp lệ không
uuid_rfc4122Xác thực giá trị trường có phải là UUID RFC4122 hợp lệ bất kỳ phiên bản nào không
md4Xác thực giá trị trường có phải là MD4 hợp lệ không
md5Xác thực giá trị trường có phải là MD5 hợp lệ không
sha256Xác thực giá trị trường có phải là SHA256 hợp lệ không
sha384Xác thực giá trị trường có phải là SHA384 hợp lệ không
sha512Xác thực giá trị trường có phải là SHA512 hợp lệ không
ripemd128Xác thực giá trị trường có phải là RIPEMD128 hợp lệ không
ripemd160Xác thực giá trị trường có phải là RIPEMD160 hợp lệ không
tiger128Xác thực giá trị trường có phải là TIGER128 hợp lệ không
tiger160Xác thực giá trị trường có phải là TIGER160 hợp lệ không
tiger192Xác thực giá trị trường có phải là TIGER192 hợp lệ không
semverXác thực giá trị trường hiện tại có phải là phiên bản semver hợp lệ theo định nghĩa Semantic Versioning 2.0.0 không
ulidXác thực giá trị trường có phải là ULID hợp lệ không

So sánh

TagMô tả
eqBằng
gtLớn hơn
gteLớn hơn hoặc bằng
ltNhỏ hơn
lteNhỏ hơn hoặc bằng
neKhông bằng

Khác

TagMô tả
dirThư mục file
fileĐường dẫn file
isdefaultXác thực giá trị trường hiện tại có phải là giá trị tĩnh mặc định không
lenĐộ dài trường
maxGiá trị lớn nhất
minGiá trị nhỏ nhất
oneofCó phải là một trong các giá trị liệt kê không
omitemptyNếu trường không được đặt, bỏ qua trường đó
requiredGiá trị bắt buộc
required_ifChỉ khi tất cả các trường được chỉ định khác bằng với giá trị được chỉ định, trường xác thực phải tồn tại và không rỗng
required_unlessTrừ khi tất cả các trường được chỉ định khác bằng với giá trị được chỉ định, trường xác thực phải tồn tại và không rỗng
required_withKhi bất kỳ trường được chỉ định nào tồn tại, trường xác thực phải tồn tại và không rỗng
required_with_allKhi tất cả các trường được chỉ định đều tồn tại, trường xác thực phải tồn tại và không rỗng
required_withoutKhi bất kỳ trường được chỉ định nào không tồn tại, trường xác thực phải tồn tại và không rỗng
required_without_allKhi tất cả các trường được chỉ định đều không tồn tại, trường xác thực phải tồn tại và không rỗng
excluded_ifChỉ khi tất cả các trường được chỉ định khác bằng với giá trị được chỉ định, trường có thể không tồn tại hoặc rỗng
excluded_unlessTrừ khi tất cả các trường được chỉ định khác bằng với giá trị được chỉ định, trường có thể không tồn tại hoặc rỗng
excluded_withKhi bất kỳ trường được chỉ định nào tồn tại, trường có thể không tồn tại hoặc rỗng
excluded_with_allKhi tất cả các trường được chỉ định đều tồn tại, trường có thể không tồn tại hoặc rỗng
excluded_withoutKhi bất kỳ trường được chỉ định nào không tồn tại, trường có thể không tồn tại hoặc rỗng
excluded_without_allKhi tất cả các trường được chỉ định đều không tồn tại, trường có thể không tồn tại hoặc rỗng
uniqueXác thực mỗi giá trị arr, map, slice có phải là duy nhất không

Alias

TagMô tả
iscolorhexcolor|rgb|rgba|hsl|hsla
country_codeiso3166_1_alpha2|iso3166_1_alpha3|iso3166_1_alpha_numeric

Toán tử

TagMô tảHex
,Toán tử AND, sử dụng nhiều tag xác thực, phải thỏa mãn tất cả điều kiện, không được có khoảng trắng giữa các dấu phẩy0x2c
|Toán tử OR, sử dụng nhiều tag xác thực, nhưng chỉ cần thỏa mãn một trong số đó0x7c
-Bỏ qua xác thực cho trường này0x2d
=Ký hiệu khớp tham số0x3d

TIP

Khi muốn khớp toán tử trong quá trình xác thực trường, cần sử dụng dạng hex十六进制 utf8 để thay thế, ví dụ

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

Sử dụng

Dưới đây sẽ giới thiệu một số cách sử dụng cơ bản của Validator cùng với một số ví dụ code.

Singleton

go
var validate *validator.Validate

Khi sử dụng, official khuyến nghị trong toàn bộ vòng đời chương trình, chỉ nên có một instance validator, điều này sẽ có lợi cho việc cache một số dữ liệu.

Tạo Validator

Khi sử dụng riêng Validator mà không tích hợp với các framework khác, cần tạo validator thủ công.

go
validate = validator.New()

Xác thực Struct

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

Method Struct dùng để xác thực tất cả các trường public của một struct, mặc định sẽ tự động xác thực struct lồng nhau, khi truyền giá trị không hợp lệ hoặc giá trị là nil, sẽ trả về InvalidValidationError, nếu lỗi xác thực thất bại thì trả về ValidationErrors.

Ví dụ

go
package validate

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

type User struct {
  Name    string `validate:"contains=jack"` // Tên chứa jack
  Age     int    `validate:"gte=18"`        // Lớn hơn hoặc bằng 18 tuổi
  Address string `validate:"endswith=市"`    // Kết thúc bằng 市
}

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()) // 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)
}

Output

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

Xác thực Map

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

Xác thực key-value thông qua tag map.

Ví dụ

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)
}

Output

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

Xác thực Slice

Xác thực string slice, trước dive là tag xác thực cho slice, sau dive là tag xác thực cho giá trị trong slice, slice lồng nhau cũng tương tự, có bao nhiêu chiều thì dùng bấy nhiêu 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") // Độ dài slice tối đa là 5, phần tử phải chứa ký tự a, và độ dài tối thiểu là 5
  fmt.Println(err)
}

Output

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

Xác thực struct cho từng user trong slice

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" nghĩa là xác thực sâu, khi phần tử là struct, sẽ tự động xác thực struct
   fmt.Println(err)
}

Output

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

Xác thực Variable

Khá đơn giản và dễ hiểu, không giải thích nhiều

Ví dụ 1

go
func TestVar(t *testing.T) {
   name := "jack"
   err := validator.New().Var(name, "max=5,contains=a,min=1,endswith=l") // Độ dài tối đa là 5, độ dài tối thiểu là 1, chứa ký tự a, kết thúc bằng ký tự l
   fmt.Println(err)
}

Output

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

Ví dụ 2

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

Output

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

TIP

Method Var có thể xác thực các kiểu bao gồm struct, variable, slice, map, cần kết hợp hợp lý với tag dive để sử dụng.

Xác thực Field

Tham số của xác thực field không còn là kiểu cơ bản, mà là tên trường của struct, có thể là trường của chính nó, cũng có thể là trường của struct lồng nhau.

go
type Password struct {
   FirstPassword  string `validate:"eqfield=SecondPassword"` // Xác thực hai lần nhập mật khẩu có bằng nhau không
   SecondPassword string
}

type RegisterUser struct {
   Username string `validate:"necsfield=Password.FirstPassword"` // Khi đăng ký để đảm bảo an toàn, cấm mật khẩu và tên người dùng giống nhau
   Password Password
}

func TestCrossStructFieldValidate(t *testing.T) {
   validate = validator.New()
   // Thất bại
   fmt.Println(validate.Struct(RegisterUser{
      Username: "gopher",
      Password: Password{
         FirstPassword:  "gopher",
         SecondPassword: "gophers",
      },
   }))
   // Thành công
   fmt.Println(validate.Struct(RegisterUser{
      Username: "gophers",
      Password: Password{
         FirstPassword:  "gopher",
         SecondPassword: "gopher",
      },
   }))
}

Output

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

Khi sử dụng xác thực field, khi trường hoặc struct được truyền vào như tham số Tag không tồn tại, sẽ được coi là xác thực thất bại, ví dụ:

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

Đối với lỗi chính tả như thế này, rất khó phát hiện, và khi xác thực cũng chỉ hiển thị dưới dạng không vượt qua, cần rất chú ý.

Nâng cao

Tiếp theo sẽ giải thích một số kỹ thuật sử dụng nâng cao và nhiều thao tác tùy chỉnh hơn.

Alias Tùy chỉnh

Trong một số trường hợp, đối với một trường có rất nhiều tag xác thực, khi bạn muốn tái sử dụng cho trường khác, bạn có thể sao chép dán, nhưng đây không phải là cách tốt nhất, cách tốt hơn là cải thiện tính tái sử dụng thông qua việc đăng ký alias, hãy xem ví dụ sau:

go
var validate *validator.Validate

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

func TestAlias(t *testing.T) {
  validate = validator.New()
    // Đăng ký alias
  validate.RegisterAlias("namerules", PERSON_NAME_RULES)
  type person struct {
    FirstName string `validate:"namerules"` // Sử dụng alias
    LastName  string `validate:"namerules"`
  }

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

  fmt.Println(err)
}

Output

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

Hàm Xác thực Tùy chỉnh

Mặc dù các tag xác thực đi kèm với component đã đủ đáp ứng cho hầu hết các trường hợp cơ bản, nhưng đôi khi đối với một số nhu cầu đặc biệt cần tự định nghĩa logic, Validator cung cấp cho chúng ta các API liên quan để tùy chỉnh hàm xác thực. Hãy xem ví dụ sau:

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"
}

Tạo một hàm để kiểm tra xem giá trị trường có bằng "666" không, và tag tương ứng của nó là is666, output như sau

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

TIP

Nếu tag đã đăng ký đã tồn tại, nó sẽ bị ghi đè bởi cái mới, nói cách khác có thể "viết lại" logic kiểm tra tag mặc định.

Hàm Xác thực Kiểu Tùy chỉnh

Hàm xác thực kiểu là dành riêng cho một kiểu nào đó, thường được sử dụng cho một số kiểu không phải kiểu cơ bản, tương tự cũng có thể ghi đè kiểm tra mặc định của kiểu cơ bản, hãy xem ví dụ sau:

go
type Address struct {
  name string
}

func TestCustomTypeValidate(t *testing.T) {
  validate = validator.New()
  validate.RegisterCustomTypeFunc(ValidateAddress, Address{}) // Đăng ký hàm xác thực kiểu và kiểu tương ứng
  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 {
    // Xử lý lỗi
    if address.name == "" {
      return address.name
    }

    return value // Trả về trường nghĩa là xác thực đúng
  }
  return nil
}

Output

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

TIP

Đồng thời đăng ký nhiều kiểu vào một hàm cũng tương tự như vậy

Hàm Xác thực Struct Tùy chỉnh

Sự khác biệt của hàm xác thực struct là ở chỗ, tham số của các hàm khác là trường, còn tham số của hàm này là struct, hãy xem ví dụ sau:

go
type People struct {
   FirstName string
   LastName  string
}

func TestCustomStructLevel(t *testing.T) {
   validate = validator.New()
   validate.RegisterStructValidation(PeopleValidate, People{}) // Giống như đăng ký kiểu, có thể truyền vào không chỉ một loại struct
   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", "", "")
   }
}

Output

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

Đa ngôn ngữ

Component translator

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

Component region

go get github.com/go-playground/locales

Ngôn ngữ mặc định của validator là tiếng Anh, mà khi chúng ta phát triển dự án, có thể sẽ sử dụng nhiều hơn một ngôn ngữ, lúc này chúng ta cần sử dụng component quốc tế hóa đa ngôn ngữ, hãy xem ví dụ sau:

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"` // Tên chứa jack
   Age     int    `validate:"gte=18"`        // Lớn hơn hoặc bằng 18 tuổi
   Address string `validate:"endswith=市"`    // Kết thúc bằng 市
}

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

func TestTranslate(t *testing.T) {
   zh := zh.New()
   // Đầu tiên là dự phòng, sau đó là các ngôn ngữ được hỗ trợ, có thể có nhiều
   uni = ut.New(zh, zh)
   // Ngôn ngữ ở đây thường có thể lấy từ Accept-Language trong header của http request
   trans, found := uni.GetTranslator(zh.Locale())
   validate = validator.New()
   if found {
      zh_trans.RegisterDefaultTranslations(validate, trans) // Đăng ký translator mặc định
   }
   err := validate.Struct(User{
      Name:    "",
      Age:     0,
      Address: "",
   })
   fmt.Println(err.(validator.ValidationErrors).Translate(trans))
}

Output

map[User.Address:Address phải kết thúc bằng văn bản '市' User.Age:Age phải lớn hơn hoặc bằng 18 User.Name:Name phải chứa văn bản 'jack']

Cũng có thể dịch riêng từng lỗi

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

Output

Name phải chứa văn bản 'jack'
Age phải lớn hơn hoặc bằng 18
Address phải kết thúc bằng văn bản '市'

Có thể thấy giá trị trả về là một map, có thể thấy thông tin lỗi cơ bản đã được dịch, nhưng chưa đủ để đưa vào sử dụng, chúng ta cần tiếp tục làm đẹp thông tin lỗi để dễ dàng对接 với khách hàng hoặc frontend hơn.

go
type User struct {

   Name    string `validate:"contains=jack" label:"姓名"` // Tên chứa jack
   Age     int    `validate:"gte=18" label:"年龄"`        // Lớn hơn hoặc bằng 18 tuổi
   Address string `validate:"endswith=市" label:"地址"`    // Kết thúc bằng 市
   Sex     string `validate:"required" label:"性别"`
}

Đầu tiên tùy chỉnh tag label, giá trị của nó là tên tiếng Trung của trường, sau đó đăng ký một TagNameFunc thông qua validator, tác dụng của nó là khi lấy tên trường hoặc thay thế tên gốc. Chú thích trên method Filed() string trong file errors.go nói rằng: "Tên trường với label name được ưu tiên hơn tên trường thực tế", vì vậy khi xảy ra lỗi sau này, có thể sử dụng tên tiếng Trung tùy chỉnh để thay thế từ tiếng Anh. TagNameFunc như sau:

go
// Chúng ta thêm một tag tùy chỉnh, tag này dùng để đặt tên tiếng Trung cho trường struct, nó sẽ thay thế tên trường gốc
func CustomTagNameFunc(field reflect.StructField) string {
   label := field.Tag.Get("label")
   if len(label) == 0 {
      return field.Name
   }
   return label
}

Cuối cùng đăng ký

go
validate.RegisterTagNameFunc(CustomTagNameFunc)

Thực thi lại output

Tên phải chứa văn bản 'jack'
Tuổi phải lớn hơn hoặc bằng 18
Địa chỉ phải kết thúc bằng văn bản '市'

Sau đó vẫn chưa đủ, vẫn chưa đủ để làm thông tin lỗi trả về cho frontend, chúng ta cần định dạng thành json hoặc bất kỳ định dạng nào phù hợp cho việc truyền tải tin nhắn, bạn có thể nghĩ đến việc serialize map thành json, đây là một giải pháp, nhưng bạn có thể nhận được kết quả như sau:

json
{
  "User.地址": "Địa chỉ phải kết thúc bằng văn bản '市'",
  "User.姓名": "Tên phải chứa văn bản 'back'",
  "User.年龄": "Tuổi phải lớn hơn hoặc bằng 18",
  "User.性别": "Giới tính là trường bắt buộc"
}

Bằng cách xử lý key của map, nhận được kết quả như sau:

json
{
  "地址": "Địa chỉ phải kết thúc bằng văn bản '市'",
  "姓名": "Tên phải chứa văn bản 'back'",
  "年龄": "Tuổi phải lớn hơn hoặc bằng 18",
  "性别": "Giới tính là trường bắt buộc"
}

Tuy nhiên không khuyến nghị trả về thông tin như trên cho frontend, chúng ta có thể xử lý thành một string để làm thông tin trả về

Tên phải chứa văn bản 'back', Tuổi phải lớn hơn hoặc bằng 18, Địa chỉ phải kết thúc bằng văn bản '市', Giới tính là trường bắt buộc,

Code đầy đủ

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:"姓名"` // Tên chứa jack
   Age     int    `validate:"gte=18" label:"年龄"`        // Lớn hơn hoặc bằng 18 tuổi
   Address string `validate:"endswith=市" label:"地址"`    // Kết thúc bằng 市
   Sex     string `validate:"required" label:"性别"`
}

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

// Chúng ta thêm một tag tùy chỉnh, tag này dùng để đặt tên tiếng Trung cho trường struct, nó sẽ thay thế tên trường gốc
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)
   // Ngôn ngữ ở đây thường có thể lấy từ Accept-Language trong header của http request
   trans, found := uni.GetTranslator(zh.Locale())
   validate = validator.New()
   if found {
      zh_trans.RegisterDefaultTranslations(validate, trans) // Đăng ký translator mặc định
   }
   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()
}

Cuối cùng, nếu cảm thấy thông tin lỗi quá lạnh lùng, muốn nhân tính hóa hơn, có thể viết lại thông báo lỗi của tag chỉ định, điều này cần sử dụng method RegisterTranslation, đồng thời cũng cần sử dụng hai loại hàm, lần lượt là RegisterTranslationsFunc chịu trách nhiệm đăng ký template dịch cho tag tương ứng, còn TranslationFunc thì chịu trách nhiệm xử lý template để có được nội dung dịch cuối cùng. Ở đây lấy required làm ví dụ:

go
func requiredOverrideRegister(ut ut.Translator) error { // Hàm này có tác dụng đăng ký template dịch
  return ut.Add("required", "{} là một trường bắt buộc phải điền", true) // {} là placeholder, true nghĩa là ghi đè template đã có
}

func requiredOverrideTranslation(ut ut.Translator, fe validator.FieldError) string { // Hàm này chịu trách nhiệm dịch nội dung
  t, _ := ut.T("required", fe.Field()) // Tham số có thể có nhiều, tùy thuộc vào số lượng placeholder trong template của tag tương ứng đã đăng ký
  return t
}

Cuối cùng đăng ký

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

Kết quả

Tên phải chứa văn bản 'back', Tuổi phải lớn hơn hoặc bằng 18, Địa chỉ phải kết thúc bằng văn bản '市', Giới tính là một trường bắt buộc phải điền,

File Ngôn ngữ

Trên thực tế, việc đăng ký từng cái bằng code rất phức tạp, universal-translator cung cấp cách thức thông qua việc viết file cấu hình JSON để dịch: 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") // Khuyến nghị import sau khi đăng ký, như vậy mới có thể ghi đè tag gốc
   if er != nil {
      log.Fatal(er)
   }
   type Gopher struct {
      Language string `validate:"required"`
   }

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

File JSON

json
[
  {
    "locale": "zh",
    "key": "required",
    "trans": "Đây là một trường rất quan trọng{0}, bạn phải điền nó",
    "override": true
  }
]

Output

map[Gopher.Language:Đây là một trường rất quan trọngLanguage, bạn phải điền nó]

TIP

universal-translator có rất nhiều cạm bẫy khi sử dụng, nếu muốn ghi đè tag gốc, typerule đều có thể không điền, vì trong cấu hình gốc cũng không điền, tốt nhất nên giữ nhất quán. Điền type gì, sẽ thêm cấu hình vào map tương ứng, nếu là Cardinal hoặc type khác và rule được cấu hình one之类的, thì cần cấu hình tương ứng trong locale mới có thể sử dụng bình thường, nếu không sẽ báo lỗi.

Golang by www.golangdev.cn edit