Home > Software design >  Golang - Access to struct property dynamically by name
Golang - Access to struct property dynamically by name

Time:08-24

I have struct of configuration like this(in short version):

type Config struct {
    Environment        string
    Service1           Service
    Service2           Service
}

type Service struct {
    CRC        string
    Cards      Cards
}

type Cards struct {
    GBP CardCfg
    USD CardCfg
}

type CardCfg struct {
    CRC        string
}

func Cfg() *Config {
    return &Config{
        Environment: os.Getenv("ENVIRONMENT"),
        Service1: Service{
            CRC: os.Getenv("Service1_CRC"),
            Cards: Cards{
                GBP: CardCfg{
                    CRC: os.Getenv("Service1_CARD_GBP_CRC"),
                },
                USD: CardCfg{
                    CRC: os.Getenv("Service1_CARD_USD_CRC"),
                },
            },
        },

        Service2: Service{
            CRC: os.Getenv("Service2_CRC"),
            Cards: Cards{
                GBP: CardCfg{
                    CRC: os.Getenv("Service2_CARD_GBP_CRC"),
                },
                USD: CardCfg{
                    CRC: os.Getenv("Service2_CARD_USD_CRC"),
                },
            },
        },
    }
}

I try to get access to service crc or service card crc by variable like this:

variable := "Service1"
currency := "EUR"

cfg := config.Cfg()

crc := cfg[variable].cards[currency] // DOESN'T WORK

I always tried with map, like this:

package main

import "fmt"

type Config map[string]interface{}

func main() {
    config := Config{
        "field": "value",
        "service1": Config{
            "crc": "secret1",
            "cards": Config{
                "crc": "secret2",
            },
        },
    }

    fmt.Println(config["WT"].(Config)["cards"].(Config)["crc"]) //WORK
}

but it looks wierd for me. Do you know better way to write config? It's possible to use struct? I come form Ruby planet, Golang is new for me.

edit:

I receive messages from rabbit queue, based on them I create a payment. Unfortunately, various payment methods require "own" authorization (crc and merchantId). Call looks like this:

    trn, err := p24Client.RegisterTrn(context.Background(), &p24.RegisterTrnReq{
        CRC:                        cfg[payinRequested.Service].cards[payinRequested.Currency].CRC,
        MerchantId:                 cfg[payinRequested.Service].cards[payinRequested.Currency].MerchantId,
        PosId:                      cfg[payinRequested.Service].cards[payinRequested.Currency].MerchantId,
        SessionId:                  payinRequested.PaymentId,
        Amount:                     payinRequested.Amount,
        Currency:                   payinRequested.Currency,
        Description:                payinRequested.Desc,
        Email:                      payinRequested.Email,
        Method:                     payinRequested.BankId,
        UrlReturn:                  payinRequested.ReturnUrl,
        UrlStatus:                  cfg.StatusUri,
        UrlCardPaymentNotification: cfg.CardStatusUri,
    })

Any ideas on how to do it right?

CodePudding user response:

Ignoring the reflect package, the simple answer is: you can't. You cannot access struct fields dynamically (using string variables). You can, use variables on a map, because accessing data in a map is a hashtable lookup. A struct isn't.

I will reiterate the main point of my comments though: What you're seemingly trying to do is using environment variables to set values on a config struct. This is very much a solved problem. We've been doing this for years at this point. I did a quick google search and found this repo which does exactly what you seem to want to do (and more): called configure

With this package, you can declare your config struct like this:

package config

type Config struct {
    Environment   string     `env:"ENVIRONMENT" cli:"env" yaml:"environment"`
    Services      []*Service `env:"SERVICE" cli:"service" yaml:"service"`
    serviceByName map[string]*Service
}

Then, to load from environment variables:

func LoadEnv() (*Config, err) {
    c := Config{
         serviceByName: map[string]*Service{},
    } // set default values if needed
    if err := configure.ParseEnv(&c); err != nil {
        return nil, err
    }
    // initialise convenience fields like serviceByName:
    for _, svc := range c.Services {
        c.serviceByName[svc.Name] = svc
    }
    return &c, nil
}

// ServiceByName returns a COPY of the config for a given service
func (c Config) ServiceByName(n string) (Service, error) {
    s, ok := c.serviceByName[n]
    if !ok {
        return nil, errrors.New("service with given name does not exist")
    }
    return *s, nil
}

You can also define a single Load function that will prioritise one type of config over the other. With these tags, we're supporting environment variables, a Yaml file, and command line arguments. Generally command line arguments override any of the other formats. As for Yaml vs environment variables, you could argue both ways: an environment variable like ENVIRONMENT isn't very specific, and could easily be used by multiple processes by mistake. Then again, if you deploy things properly, that shouldn't be an issue, so for that reason, I'd prioritise environment variables over the Yaml file:

func Load(args []string) (*Config, error) {
    c := &Config{
        Environment:   "devel", // default
        serviceByName: map[string]*Service{},
    }
    if err := configure.ParseYaml(c); err != nil {
        return nil, err
    }
    if err := configure.ParseEnv(c); err != nil {
        return nil, err
    }
    if len(args) > 0 {
        if err := configure.ParseCommanLine(c, args); err != nil {
            return nil, err
        }
    }
    // initialise convenience fields like serviceByName:
    for _, svc := range c.Services {
        c.serviceByName[svc.Name] = svc
    }
    return &c, nil
}

Then in your main package:

func main() {
    cfg, err := config.Load(os.Args[1:])
    if err != nil {
        fmt.Printf("Failed to load config: %v\n", err)
        os.Exit(1)
    }
    wtCfg, err := config.ServiceByName("WT")
    if err != nil {
        fmt.Printf("WT service not found: %v\n", err)
        return
    }
    fmt.Printf("%#v\n", wtCfg)
}
  • Related