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 реализует валидатор значений на основе тегов структур со следующими уникальными возможностями:

  • Можно использовать теги валидации и пользовательские валидаторы для межполевой и межструктурной валидации

  • Слайсы, массивы, карты или любые многомерные поля могут быть валидированы

  • Можно погружаться в валидацию ключей и значений карт

  • Определяет, как обрабатывать перед валидацией по его базовому типу

  • Может обрабатывать пользовательские типы полей

  • Поддерживает теги псевдонимов, которые позволяют сопоставить несколько валидаций одному тегу для более простого определения валидации структур

  • Может извлекать пользовательские имена полей, такие как JSON-имена для отображения в сообщениях об ошибках

  • Пользовательские многоязычные сообщения об ошибках

  • Стандартный компонент валидации по умолчанию для фреймворка gin

Установка

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

Импорт

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

Теги

Валидатор имеет множество базовых тегов валидации. Все функции валидации, соответствующие тегам, можно найти в файле baked_in.go. Тег структуры для валидатора — validate.

Например

go
type User {
  age int `validate:"gte=18"` // означает больше или равно 18 годам
}

Вы также можете изменить тег по умолчанию через метод setTagName.

Поля

TagОписание
eqcsfieldВ отдельной структуре валидирует, что значение текущего поля равно значению поля, указанного в параметре
eqfieldВалидирует, что значение текущего поля равно значению поля, указанного в параметре
fieldcontainsВалидирует, что значение текущего поля содержит значение поля, указанного в параметре
fieldexcludesВалидирует, что значение текущего поля не содержит значение поля, указанного в параметре
gtcsfieldВ отдельной структуре валидирует, что значение текущего поля больше значения поля, указанного в параметре
gtecsfieldВ отдельной структуре валидирует, что значение текущего поля больше или равно значению поля, указанного в параметре
gtefieldВалидирует, что значение текущего поля больше или равно значению поля, указанного в параметре
gtfieldВалидирует, что значение текущего поля больше значения поля, указанного в параметре
ltcsfieldВ отдельной структуре валидирует, что значение текущего поля меньше значения поля, указанного в параметре
ltecsfieldВ отдельной структуре валидирует, что значение текущего поля меньше или равно значению поля, указанного в параметре
ltefieldВалидирует, что значение текущего поля меньше или равно значению поля, указанного в параметре
ltfieldВалидирует, что значение текущего поля меньше значения поля, указанного в параметре
necsfieldВалидирует, что значение текущего поля не равно значению поля в отдельной структуре, указанному в параметре
nefieldВалидирует, что значение текущего поля не равно значению поля, указанного в параметре

Сеть

TagОписание
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Адрес управления доступом к среде, также известный как LAN-адрес
tcp4_addrАдрес протокола управления передачей TCP4
tcp6_addrАдрес протокола управления передачей TCPv6
tcp_addrАдрес протокола управления передачей TCP
udp4_addrАдрес пользовательского дейтаграммного протокола UDPv4
udp6_addrАдрес пользовательского дейтаграммного протокола UDPv6
udp_addrАдрес пользовательского дейтаграммного протокола UDP
unix_addrАдрес конечной точки доменного сокета Unix
uriИдентификатор ресурса Uniform Resource Identifier
urlЛокатор ресурса Uniform Resource Locator
url_encodedКодирование идентификатора ресурса Uniform Resource Identifier encoding
urn_rfc2141Имя ресурса Uniform Resource Name RFC 2141

Строки

TagОписание
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Описание
base64Строка Base64
base64urlСтрока Base64URL
bicВалидирует, что значение текущего поля является допустимым BIC-кодом (SWIFT-кодом) как определено в ISO 9362
bcp47_language_tagВалидирует, что значение текущего поля является языковым тегом BCP47
btc_addrВалидирует, что значение поля является допустимым BTC-адресом
btc_addr_bech32Валидирует, что значение поля является допустимым bech32 BTC-адресом
credit_cardВалидирует, что значение текущего поля является допустимым номером кредитной карты
datetimeВалидирует, что значение текущего поля является допустимой строкой datetime
e164Валидирует, что значение текущего поля является допустимым форматом номера телефона e.164
emailВалидирует, что значение текущего поля является допустимым адресом электронной почты
eth_addrВалидирует, что значение поля является допустимым Ethereum-адресом
hexadecimalВалидирует, что значение текущего поля является допустимым шестнадцатеричным
hexcolorВалидирует, что значение текущего поля является допустимым hex-цветом
hslВалидирует, что значение текущего поля является допустимым цветом HSL
hslaВалидирует, что значение текущего поля является допустимым цветом HSLA
htmlВалидирует, что значение текущего поля является допустимым HTML
html_encodedВалидирует, что значение текущего поля является допустимым HTML-кодированным
isbnВалидирует, что значение поля является допустимым ISBN v10 или v13 (International Standard Book Number)
isbn10Валидирует, что значение поля является допустимым ISBN v10
isbn13Валидирует, что значение поля является допустимым ISBN v13
iso3166_1_alpha2Валидирует, что значение текущего поля является допустимым кодом страны iso3166-1 alpha-2
iso3166_1_alpha3Валидирует, что значение текущего поля является допустимым кодом страны iso3166-1 alpha-3
iso3166_1_alpha_numericВалидирует, что значение текущего поля является допустимым alphanumeric-кодом страны 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Валидирует, что значение текущего поля является допустимой строкой 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Валидирует, что значение поля является допустимым RIPEMD128
ripemd160Валидирует, что значение поля является допустимым RIPEMD160
tiger128Валидирует, что значение поля является допустимым TIGER128
tiger160Валидирует, что значение поля является допустимым TIGER160
tiger192Валидирует, что значение поля является допустимым TIGER192
semverВалидирует, что значение текущего поля является допустимой версией semver как определено в Semantic Versioning 2.0.0
ulidВалидирует, что значение поля является допустимым ULID

Сравнение

TagОписание
eqРавно
gtБольше чем
gteБольше или равно
ltМеньше чем
lteМеньше или равно
neНе равно

Другое

TagОписание
dirКаталог файлов
fileПуть к файлу
isdefaultВалидирует, что значение текущего поля является значением по умолчанию статическим
lenДлина поля
maxМаксимальное значение
minМинимальное значение
oneofЯвляется ли одним из перечисленных значений
omitemptyЕсли поле не установлено, игнорировать поле
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Описание
iscolorhexcolor|rgb|rgba|hsl|hsla
country_codeiso3166_1_alpha2|iso3166_1_alpha3|iso3166_1_alpha_numeric

Операторы

TagОписаниеHex
,Оператор AND, используйте несколько тегов валидации, все условия должны быть выполнены, без пробелов между запятыми0x2c
|Оператор OR, используйте несколько тегов валидации, но только один должен быть выполнен0x7c
-Пропустить валидацию для этого поля0x2d
=Символ сопоставления параметров0x3d

TIP

При сопоставлении операторов во время валидации поля нужно заменить их на шестнадцатеричное представление utf8, например

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

Использование

Далее будет представлено некоторое базовое использование Validator с примерами кода.

Singleton

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"`        // Больше или равно 18 годам
  Address string `validate:"endswith=市"`    // Заканчивается на 市 (город)
}

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

Валидация карт

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

Валидирует пары ключ-значение через тег map.

Пример

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 валидируют слайс, теги после dive валидируют значения в слайсе. Вложенные слайсы следуют тому же принципу — используйте столько тегов 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

go
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 может валидировать типы включая структуры, переменные, слайсы и карты. Используйте его разумно в сочетании с тегом 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
}

Для таких ошибок написания трудно обнаружить, и во время валидации это покажет только как неудача валидации. Это требует особого внимания.

Продвинутое использование

Далее мы объясним некоторые продвинутые приёмы использования и больше пользовательских операций.

Пользовательские псевдонимы

Иногда у поля много тегов валидации. Когда вы хотите переиспользовать их на другом поле, вы можете просто скопировать и вставить, но это не лучшее решение. Лучший подход — зарегистрировать псевдонимы для улучшения переиспользования. См. пример ниже:

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

Пользовательские функции валидации

Хотя встроенные теги валидации компонента достаточны для базовых сценариев, иногда для специальных требований вы должны определить свою собственную логику. 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", и её соответствующий тег — is666. Вывод следующий:

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

TIP

Если зарегистрированный тег уже существует, он будет перезаписан существующим. Другими словами, вы можете "переписать" логику валидации тега по умолчанию.

Пользовательские функции валидации типов

Функции валидации типов специально предназначены для определённых типов, обычно для небазовых типов. Аналогично, вы также можете переопределить валидацию базового типа по умолчанию. См. пример ниже:

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"`        // Больше или равно 18 годам
   Address string `validate:"endswith=市"`    // Заканчивается на 市 (город)
}

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

func TestTranslate(t *testing.T) {
   zh := zh.New()
   // Первый — резервный, последующие — поддерживаемые языки, может быть несколько
   uni = ut.New(zh, zh)
   // Язык здесь обычно получается из заголовка Accept-Language в HTTP-запросах
   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 must end with text '市' User.Age:Age must be greater than or equal to 18 User.Name:Name must contain text 'jack']

Вы также можете перевести каждую ошибку индивидуально

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

Вывод

Name must contain text 'jack'
Age must be greater than or equal to 18
Address must end with text '市'

Можно видеть, что возвращаемое значение — это map. Базовый перевод сообщений об ошибках достигнут, но ещё не готово для использования в продакшене. Нам нужно дополнительно улучшить сообщения об ошибках для лучшей интеграции с клиентами или фронтендом.

go
type User struct {

   Name    string `validate:"contains=jack" label:"姓名"` // Имя содержит jack
   Age     int    `validate:"gte=18" label:"年龄"`        // Больше или равно 18 годам
   Address string `validate:"endswith=市" label:"地址"`    // Заканчивается на 市 (город)
   Sex     string `validate:"required" label:"性别"`
}

Сначала кастомизируйте тег label. Его значение — китайское имя поля. Затем зарегистрируйте TagNameFunc в валидаторе. Его роль — получать имя поля или заменять оригинальное имя. Комментарий к методу Field() string в файле errors.go гласит: "Имя поля с тегом label имеет приоритет над фактическим именем поля". Поэтому при возникновении ошибок мы можем использовать пользовательское китайское имя для замены английского слова. 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)

Выполните снова, вывод

姓名 must contain text 'jack'
年龄 must be greater than or equal to 18
地址 must end with text '市'

Но этого ещё недостаточно. Ещё не подходит как информация об ошибках, возвращаемая фронтенду. Нам нужно отформатировать информацию в JSON или любой формат, подходящий для передачи сообщений. Вы можете подумать о прямой сериализации map в JSON, это решение, но вы можете получить следующий результат:

json
{
  "User.地址": "地址 must end with text '市'",
  "User.姓名": "姓名 must contain text 'back'",
  "User.年龄": "年龄 must be greater than or equal to 18",
  "User.性别": "性别 is required"
}

Обработав значения ключей map, получите следующий результат:

json
{
  "地址": "地址 must end with text '市'",
  "姓名": "姓名 must contain text 'back'",
  "年龄": "年龄 must be greater than or equal to 18",
  "性别": "性别 is required"
}

Однако не рекомендуется возвращать такую информацию фронтенду. Мы можем обработать её в строку как сообщение возврата:

姓名 must contain text 'back', 年龄 must be greater than or equal to 18, 地址 must end with text '市', 性别 is required,

Полный код

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:"年龄"`        // Больше или равно 18 годам
   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)
   // Язык здесь обычно получается из заголовка Accept-Language в HTTP-запросах
   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()
}

Наконец, если вы считаете, что сообщения об ошибках слишком холодные и хотите их более гуманизированными, вы можете переписать сообщения об ошибках для определённых тегов. Это требует использования метода RegisterTranslation, а также требует двух типов функций: RegisterTranslationsFunc, отвечающей за регистрацию шаблона перевода для соответствующего тега, и TranslationFunc, отвечающей за обработку шаблона для получения финального содержимого перевода. Вот пример с использованием required:

go
func requiredOverrideRegister(ut ut.Translator) error { // Роль этой функции — зарегистрировать шаблон перевода
  return ut.Add("required", "{} is a required field", true) // {} — заполнитель, true представляет, переопределять ли существующий шаблон
}

func requiredOverrideTranslation(ut ut.Translator, fe validator.FieldError) string { // Роль этой функции — перевод содержимого
  t, _ := ut.T("required", fe.Field()) // Параметры могут быть множественными, в зависимости от того, сколько заполнителей имеет шаблон зарегистрированного тега
  return t
}

Наконец зарегистрируйте

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

Результат

姓名 must contain text 'back', 年龄 must be greater than or equal to 18, 地址 must end with text '市', 性别 is a required field,

Языковые файлы

Фактически, регистрировать один за другим с помощью кода очень утомительно. 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") // Рекомендуется импортировать после регистрации, чтобы можно было переопределить существующие теги
   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, type и rule могут быть оставлены пустыми, потому что оригинальная конфигурация также не имеет их. Лучше всего сохранять согласованность. Whatever type вы заполните, будет добавлено в соответствующую map. Если это Cardinal или другой type и rule настроен с one или подобным, тогда вам нужно сделать соответствующие конфигурации локально для нормального использования, иначе это сообщит об ошибке.

Golang by www.golangdev.cn edit