mapstructure для Go: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

mapstructure для Go: полное руководство по маппингу map в структуры

Что это и зачем нужна

Библиотека mapstructure для языка Go решает одну из самых частых задач при работе с динамическими данными: преобразование неструктурированных map[string]interface{} в строго типизированные Go-структуры. В реальных проектах данные часто приходят в виде JSON, YAML, TOML или из внешних API, где структура может быть не гарантирована. Стандартный encoding/json справляется с JSON, но когда вы работаете с произвольными map (например, из конфигурационных файлов, Redis Hash, или результатов SQL-запросов), вам нужен инструмент, который умеет гибко и безопасно маппить данные.

Основная задача mapstructure — взять map[строки]интерфейсов и заполнить поля структуры, автоматически преобразуя типы, поддерживая вложенные структуры, слайсы, указатели и даже пользовательские типы. Библиотека особенно полезна при парсинге конфигураций, где ключи могут быть в snake_case, а поля структуры — в CamelCase, или когда нужно десериализовать данные из источников, не поддерживающих стандартную JSON-схему.

Ключевое преимущество mapstructure перед ручным маппингом — декларативность. Вы описываете структуру с тегами, а библиотека сама заботится о рекурсивном обходе, преобразовании типов (например, string в int), обработке опциональных полей и ошибках несоответствия типов. Это делает код чище, безопаснее и значительно сокращает количество boilerplate-кода.

Установка

Для установки библиотеки используйте стандартный менеджер пакетов Go. Убедитесь, что у вас инициализирован модуль (go mod init).

go get github.com/mitchellh/mapstructure

После выполнения команды библиотека будет добавлена в go.mod и go.sum. Импортируйте её в коде:

import "github.com/mitchellh/mapstructure"

Версия на момент написания статьи — v1.5.0. Библиотека не имеет внешних зависимостей, кроме стандартной библиотеки Go.

Быстрый старт — минимальный рабочий пример

Создадим простую структуру Person и преобразуем map[string]interface{} в экземпляр этой структуры.

package main

import (
    "fmt"
    "github.com/mitchellh/mapstructure"
)

type Person struct {
    Name string
    Age  int
    City string
}

func main() {
    input := map[string]interface{}{
        "name": "Alice",
        "age":  30,
        "city": "New York",
    }

    var person Person
    err := mapstructure.Decode(input, &person)
    if err != nil {
        panic(err)
    }

    fmt.Printf("Person: %+v\n", person)
    // Вывод: Person: {Name:Alice Age:30 City:New York}
}

Как видите, ключи map автоматически сопоставились с полями структуры (регистр не чувствителен по умолчанию). Если бы ключ назывался "AGE", он всё равно бы попал в поле Age.

Основные методы и функции

Рассмотрим 8 ключевых функций/методов библиотеки mapstructure. Каждый метод сопровождается сигнатурой, описанием и примером кода.

1. Decode

Сигнатура: func Decode(input interface{}, output interface{}) error

Что делает: Основная функция для декодирования map в структуру. Принимает любой input (обычно map[string]interface{}) и указатель на структуру output. Возвращает ошибку, если типы несовместимы или произошла другая проблема.

type Config struct {
    Host string
    Port int
}

func main() {
    data := map[string]interface{}{
        "host": "localhost",
        "port": 8080,
    }
    var cfg Config
    if err := mapstructure.Decode(data, &cfg); err != nil {
        log.Fatal(err)
    }
    fmt.Println(cfg.Host, cfg.Port) // localhost 8080
}

2. DecodeHook

Сигнатура: func DecodeHook(hooks ...DecodeHookFunc) *Decoder

Что делает: Создаёт новый Decoder с пользовательскими хуками (функциями преобразования). Хуки позволяют перехватывать и модифицировать процесс декодирования, например, преобразовывать строки в time.Time или кастомные типы.

type Event struct {
    Timestamp time.Time
    Message   string
}

// Хук для преобразования строки в time.Time
func stringToTimeHook(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) {
    if f.Kind() == reflect.String && t == reflect.TypeOf(time.Time{}) {
        return time.Parse(time.RFC3339, data.(string))
    }
    return data, nil
}

func main() {
    input := map[string]interface{}{
        "timestamp": "2024-01-15T10:30:00Z",
        "message":   "Hello",
    }
    var event Event
    decoder, _ := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
        DecodeHook: stringToTimeHook,
        Result:     &event,
    })
    decoder.Decode(input)
    fmt.Println(event.Timestamp) // 2024-01-15 10:30:00 +0000 UTC
}

3. NewDecoder

Сигнатура: func NewDecoder(config *DecoderConfig) (*Decoder, error)

Что делает: Создаёт новый экземпляр Decoder с расширенной конфигурацией. Позволяет настроить поведение: слабое преобразование типов, теги, хуки, обработку неизвестных ключей и многое другое.

type User struct {
    ID   int
    Name string
}

func main() {
    input := map[string]interface{}{
        "id":   "42", // строка вместо int
        "name": "Bob",
    }
    var user User
    config := &mapstructure.DecoderConfig{
        WeaklyTypedInput: true, // разрешить преобразование строк в числа
        Result:           &user,
    }
    decoder, err := mapstructure.NewDecoder(config)
    if err != nil {
        log.Fatal(err)
    }
    decoder.Decode(input)
    fmt.Println(user.ID) // 42 (int)
}

4. DecodeMetadata

Сигнатура: func DecodeMetadata(input interface{}, output interface{}, metadata *Metadata) error

Что делает: Декодирует map в структуру и дополнительно заполняет Metadata — информацию о том, какие ключи были использованы, а какие остались неиспользованными (unused). Полезно для валидации конфигов.

type AppConfig struct {
    Name string
    Port int
}

func main() {
    input := map[string]interface{}{
        "name": "MyApp",
        "port": 3000,
        "debug": true, // лишний ключ
    }
    var cfg AppConfig
    var metadata mapstructure.Metadata
    mapstructure.DecodeMetadata(input, &cfg, &metadata)
    
    fmt.Println("Used keys:", metadata.Keys)       // [Name Port]
    fmt.Println("Unused keys:", metadata.Unused)   // [debug]
}

5. WeakDecode

Сигнатура: func WeakDecode(input, output interface{}) error

Что делает: Аналог Decode, но со включенным слабым преобразованием типов. Позволяет, например, преобразовывать строки "true"/"false" в bool, строки в числа, числа в строки и т.д. Удобно для данных из CSV или HTML-форм.

type FormData struct {
    Active bool
    Count  int
}

func main() {
    input := map[string]interface{}{
        "active": "true",   // строка
        "count":  "10",     // строка
    }
    var data FormData
    mapstructure.WeakDecode(input, &data)
    fmt.Println(data.Active, data.Count) // true 10
}

6. DecodePath

Сигнатура: func DecodePath(input map[string]interface{}, path string, output interface{}) error

Что делает: Декодирует значение по указанному пути (разделитель — точка) из вложенной map в структуру. Позволяет извлекать глубоко вложенные данные без ручного обхода.

type Address struct {
    Street string
    Zip    string
}

func main() {
    data := map[string]interface{}{
        "user": map[string]interface{}{
            "address": map[string]interface{}{
                "street": "123 Main St",
                "zip":    "10001",
            },
        },
    }
    var addr Address
    mapstructure.DecodePath(data, "user.address", &addr)
    fmt.Println(addr.Street) // 123 Main St
}

7. DecodeStruct

Сигнатура: func DecodeStruct(input interface{}, output interface{}) error

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

type Source struct {
    Name string
    Age  int
}

type Destination struct {
    FullName string `mapstructure:"name"`
    Age      int
}

func main() {
    src := Source{Name: "Charlie", Age: 25}
    var dst Destination
    mapstructure.DecodeStruct(src, &dst)
    fmt.Println(dst.FullName, dst.Age) // Charlie 25
}

8. Encode

Сигнатура: func Encode(input interface{}, output interface{}) error

Что делает: Обратная операция — преобразует структуру обратно в map[string]interface{}. Полезно для сериализации данных в динамический формат.

type Product struct {
    ID    int
    Name  string
    Price float64
}

func main() {
    p := Product{ID: 1, Name: "Laptop", Price: 999.99}
    var result map[string]interface{}
    mapstructure.Encode(p, &result)
    fmt.Println(result)
    // map[id:1 name:Laptop price:999.99]
}

Реальный практический пример

Разработаем систему загрузки конфигурации приложения из нескольких источников: YAML-файла, переменных окружения и аргументов командной строки. mapstructure поможет объединить эти данные в единую структуру.

package main

import (
    "fmt"
    "log"
    "os"
    "strconv"
    "strings"

    "github.com/mitchellh/mapstructure"
    "gopkg.in/yaml.v3"
)

// AppConfig — главная структура конфигурации
type AppConfig struct {
    Server   ServerConfig   `mapstructure:"server"`
    Database DatabaseConfig `mapstructure:"database"`
    Logging  LoggingConfig  `mapstructure:"logging"`
}

type ServerConfig struct {
    Host string `mapstructure:"host"`
    Port int    `mapstructure:"port"`
}

type DatabaseConfig struct {
    Host     string `mapstructure:"host"`
    Port     int    `mapstructure:"port"`
    User     string `mapstructure:"user"`
    Password string `mapstructure:"password"`
    DBName   string `mapstructure:"dbname"`
}

type LoggingConfig struct {
    Level  string `mapstructure:"level"`
    Format string `mapstructure:"format"`
}

// loadYAML загружает конфигурацию из YAML-файла
func loadYAML(path string) (map[string]interface{}, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, err
    }
    var result map[string]interface{}
    if err := yaml.Unmarshal(data, &result); err != nil {
        return nil, err
    }
    return result, nil
}

// loadEnv загружает переменные окружения с префиксом APP_
func loadEnv() map[string]interface{} {
    result := make(map[string]interface{})
    for _, env := range os.Environ() {
        parts := strings.SplitN(env, "=", 2)
        key := parts[0]
        value := parts[1]
        if strings.HasPrefix(key, "APP_") {
            // Преобразуем APP_SERVER_HOST в server.host
            path := strings.ToLower(strings.TrimPrefix(key, "APP_"))
            path = strings.ReplaceAll(path, "_", ".")
            setNestedValue(result, path, value)
        }
    }
    return result
}

// setNestedValue устанавливает значение по точечному пути
func setNestedValue(m map[string]interface{}, path string, value interface{}) {
    keys := strings.Split(path, ".")
    current := m
    for i, key := range keys {
        if i == len(keys)-1 {
            current[key] = value
        } else {
            if _, ok := current[key]; !ok {
                current[key] = make(map[string]interface{})
            }
            current = current[key].(map[string]interface{})
        }
    }
}

// parseArgs парсит аргументы командной строки вида --key=value
func parseArgs() map[string]interface{} {
    result := make(map[string]interface{})
    for _, arg := range os.Args[1:] {
        if strings.HasPrefix(arg, "--") {
            parts := strings.SplitN(arg[2:], "=", 2)
            if len(parts) == 2 {
                path := strings.ReplaceAll(parts[0], "-", ".")
                setNestedValue(result, path, parts[1])
            }
        }
    }
    return result
}

// mergeMaps рекурсивно объединяет несколько map
func mergeMaps(base, overlay map[string]interface{}) map[string]interface{} {
    result := make(map[string]interface{})
    for k, v := range base {
        result[k] = v
    }
    for k, v := range overlay {
        if baseVal, ok := base[k]; ok {
            if baseMap, ok := baseVal.(map[string]interface{}); ok {
                if overlayMap, ok := v.(map[string]interface{}); ok {
                    result[k] = mergeMaps(baseMap, overlayMap)
                    continue
                }
            }
        }
        result[k] = v
    }
    return result
}

// convertTypes преобразует                    

Рекомендаций