jeudi 2 août 2018

Why does Golang yaml.v2 turn my struct into a map when I construct it via reflection?

I'm working on a generic config parser that reads a YAML config file and stores the result in a struct. I'd like for the parser to be type agnostic, and I want to implement some override logic, so I'm using reflection.

Below is a complete but very simplified version of what I'm working on, which illustrates the problem around the call to yaml.Unmarshal. If I pass in a pointer to a struct that I created without reflection (base2 := TestConf{} in the example code), it works as expected: a strongly-typed struct goes in, and a strongly-typed struct comes out.

However, if I pass in a struct that I create with reflection (base := reflect.New(configType).Elem().Interface() in the example code), I pass in a struct and get a map[interface{}]interface{} back. As you can see, I've done my best to verify that the two structs are identical, panicking if their types are different or if they're not DeepEqual.

This is giving me a real headache at present, but I can work around it. I'd just like to understand why it's happening, and perhaps learn a way around it.

package main

import (
    "fmt"
    "io/ioutil"
    "os"
    "reflect"
    "time"

    yaml "gopkg.in/yaml.v2"
)

type TestConf struct {
    RequiredConfig `yaml:"RequiredConfig"`
    Str1           string     `yaml:"Str1"`
    Strptr1        *string    `yaml:"Strptr1"`
    TimePtr        *time.Time `yaml:"TimePtr"`
}

type RequiredConfig struct {
    Environment string `yaml:"Environment"`
}

var BaseConfigPath = "./config_test.yml"

func main() {
    conf := TestConf{}
    LoadConfig(&conf)
}

func LoadConfig(target interface{}) {
    targetActual := reflect.ValueOf(target).Elem()
    configType := targetActual.Type()
    base := reflect.New(configType).Elem().Interface()
    base2 := TestConf{}

    if reflect.TypeOf(base) != reflect.TypeOf(base2) {
        panic("your argument is invalid")
    }

    if !reflect.DeepEqual(base, base2) {
        panic("your argument is invalid")
    }

    if _, err := os.Stat(BaseConfigPath); !os.IsNotExist(err) {
        raw, _ := ioutil.ReadFile(BaseConfigPath)

        fmt.Printf("Before base Type: \"%v\", Kind: \"%v\"\n", reflect.TypeOf(base), reflect.ValueOf(base).Kind())
        err = yaml.Unmarshal(raw, &base)
        fmt.Printf("After base Type: \"%v\", Kind: \"%v\"\n", reflect.TypeOf(base), reflect.ValueOf(base).Kind())

        fmt.Printf("Before base2 Type: \"%v\", Kind: \"%v\"\n", reflect.TypeOf(base2), reflect.ValueOf(base2).Kind())
        err = yaml.Unmarshal(raw, &base2)
        fmt.Printf("After base2 Type: \"%v\", Kind: \"%v\"\n", reflect.TypeOf(base2), reflect.ValueOf(base2).Kind())
    }
}

To run this, you'll also need this YAML file saved at ./config_test.yml:

RequiredConfig:
  Environment: dev
Str1: String 1
Strptr1: String pointer 1
TimePtr: 2018-08-01T17:25:50.179949-04:00

The output I get:

Before base Type: "main.TestConf", Kind: "struct"
After base Type: "map[interface {}]interface {}", Kind: "map"
Before base2 Type: "main.TestConf", Kind: "struct"
After base2 Type: "main.TestConf", Kind: "struct"

So base2 acts as expected. base somehow gets converted to a map.





Aucun commentaire:

Enregistrer un commentaire