Как определить несколько тегов для полей структуры в Go?
Понимание тегов в Go
В Go теги структур — это строки метаданных, которые прикрепляются к полям и используются пакетами вроде encoding/json, encoding/xml и другими. Каждый тег — это отдельная пара "ключ:значение", разделённая пробелами. Например:
type User struct {
Name string `json:"full_name" xml:"name"`
}
Здесь json:"full_name" и xml:"name" — два разных тега для разных целей. Однако стандартный пакет encoding/json не поддерживает несколько имён для одного поля в одном теге. То есть вы не можете написать json:"full_name,username" — это будет воспринято как одно имя с запятой.
Проблема: несколько JSON-имён для одного поля
Если вам нужно, чтобы при десериализации поле Name принимало значения из JSON-ключей "full_name" и "username", а при сериализации выводилось как "full_name", стандартный encoding/json этого не умеет. Но есть несколько решений.
Решение 1: Использование кастомного типа с интерфейсом json.Unmarshaler
Создайте собственный тип, реализующий интерфейс json.Unmarshaler. Это даёт полный контроль над десериализацией:
package main
import (
"encoding/json"
"fmt"
)
type FlexibleString string
func (s *FlexibleString) UnmarshalJSON(data []byte) error {
// Убираем кавычки из JSON-строки
raw := string(data)
if len(raw) < 2 {
return fmt.Errorf("invalid string")
}
*s = FlexibleString(raw[1 : len(raw)-1])
return nil
}
type User struct {
Name FlexibleString `json:"full_name"`
Age int `json:"age"`
}
func main() {
// Десериализация из JSON с ключом "full_name"
json1 := `{"full_name": "Alice", "age": 30}`
var u1 User
json.Unmarshal([]byte(json1), &u1)
fmt.Println("full_name:", u1.Name) // Alice
// Десериализация из JSON с ключом "username"
json2 := `{"username": "Bob", "age": 25}`
var u2 User
json.Unmarshal([]byte(json2), &u2)
fmt.Println("username:", u2.Name) // Bob
}
Но этот метод не сработает, если в одном JSON-объекте есть оба ключа — возьмётся последний.
Решение 2: Использование временной структуры и ручного маппинга
Более гибкий подход — написать функцию, которая сначала десериализует JSON в map[string]interface{}, а затем вручную заполняет структуру:
package main
import (
"encoding/json"
"fmt"
)
type User struct {
Name string
Age int
}
func NewUserFromJSON(data []byte) (User, error) {
var m map[string]interface{}
if err := json.Unmarshal(data, &m); err != nil {
return User{}, err
}
u := User{}
if name, ok := m["full_name"].(string); ok {
u.Name = name
} else if name, ok := m["username"].(string); ok {
u.Name = name
}
if age, ok := m["age"].(float64); ok {
u.Age = int(age)
}
return u, nil
}
func main() {
json1 := `{"full_name": "Alice", "age": 30}`
u1, _ := NewUserFromJSON([]byte(json1))
fmt.Println(u1) // {Alice 30}
json2 := `{"username": "Bob", "age": 25}`
u2, _ := NewUserFromJSON([]byte(json2))
fmt.Println(u2) // {Bob 25}
}
Для сериализации можно добавить метод MarshalJSON:
func (u User) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]interface{}{
"full_name": u.Name,
"age": u.Age,
})
}
Решение 3: Использование сторонних библиотек
Библиотека github.com/mitchellh/mapstructure позволяет гибко маппить данные из map в структуру. А github.com/ugorji/go/codec поддерживает более сложные конфигурации тегов. Но для большинства случаев достаточно первых двух решений.
Рекомендация
- Для простых случаев (один источник JSON) — используйте стандартные теги.
- Для нескольких имён — решение с временной структурой (Solution 2) самое понятное и контролируемое.
- Для больших проектов — рассмотрите библиотеки вроде
mapstructure.
Помните, что Go — это язык с явным контролем, и часто лучше написать немного кода вручную, чем добавлять тяжёлые зависимости.
<<Альтернативный подход: использование дженериков (Go 1.18+)
Если вы используете Go 1.18 или новее, можно создать универсальную функцию для десериализации с несколькими возможными именами полей:
package main
import (
"encoding/json"
"fmt"
"reflect"
)
func UnmarshalWithAliases(data []byte, target interface{}, aliases map[string]string) error {
var rawMap map[string]json.RawMessage
if err := json.Unmarshal(data, &rawMap); err != nil {
return err
}
// Создаём новый map, заменяя алиасы на реальные имена полей
normalized := make(map[string]json.RawMessage)
for key, value := range rawMap {
if realKey, ok := aliases[key]; ok {
normalized[realKey] = value
} else {
normalized[key] = value
}
}
// Сериализуем обратно и десериализуем в целевую структуру
normalizedJSON, _ := json.Marshal(normalized)
return json.Unmarshal(normalizedJSON, target)
}
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
aliases := map[string]string{
"full_name": "name",
"username": "name",
}
json1 := `{"full_name": "Alice", "age": 30}`
var u1 User
UnmarshalWithAliases([]byte(json1), &u1, aliases)
fmt.Println(u1) // {Alice 30}
json2 := `{"username": "Bob", "age": 25}`
var u2 User
UnmarshalWithAliases([]byte(json2), &u2, aliases)
fmt.Println(u2) // {Bob 25}
}
Этот подход отделяет логику маппинга от структуры, что удобно при работе с разными API. Вы можете переиспользовать функцию UnmarshalWithAliases для любых структур, просто передавая нужные алиасы. Однако помните, что это добавляет накладные расходы на двойную сериализацию/десериализацию, поэтому для высоконагруженных систем лучше использовать ручной маппинг из первого ответа.