-2

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?

Marinso404
  • 13
  • 4
  • 5
    This is likely an XY Problem. What are you using this code for? Most likely if you really need to access a key dynamically, you want a `map` instead of a `struct`. – Adrian Aug 22 '22 at 15:25
  • 2
    Struct fields have human readable names in code only (essentially). You cannot access fields using strings just like you would in a scripting language. dynamic access like that is not possible without reflection (which you shouldn't use here). Loading config from environment values or files, though, is very much a solved problem. Plenty of options to look at. Here's the first one I found [with a simple google search](https://github.com/gravitational/configure) – Elias Van Ootegem Aug 22 '22 at 15:27
  • 3
    @Adrian: for dynamic access a map would work, but seeing as the snippets of code seem to be about loading config from environment variables anyway, I'd argue it's better the OP abandons the idea of using dynamic access all together. Config is a known set of values. Defaults/nil values can be handled cleanly, and a struct has the benefit of not needing type assertions (as opposed to `map[string]interface{}` – Elias Van Ootegem Aug 22 '22 at 15:29
  • Does [Access struct property by name](https://stackoverflow.com/q/18930910/5728991) answer your question? – Charlie Tumahai Aug 22 '22 at 15:31
  • After your update, I'm starting to feel like you didn't read my answer. Rather than `cfg[payinRequested.Service]`, with the solution I proposed, you'd just get the service config using `cfg.ServiceByName(payinRequested.Service)`, to get the card details by currency, your `Service` type would have a `ByCurrency` method that would return the relevant data (CRC, merchant ID), so `svc, err := cfg.ServiceByName(payinRequested.Service)`, then if a service was found: `card, err := svc.ByCurrency(request.Currency)`. – Elias Van Ootegem Aug 22 '22 at 17:12
  • you could even create a single method on the top level `Config.GetCard` where you pass in the service name + curency as arguments. It'll look for the service with the desired name, and then find a _"card"_ object for the requested currency. If both can be found, you'll return a `Card` object, if not, return an error: `card, err := cfg.GetCard(request.Service, request.Currency)`. The error will tell you if the service wasn't found, or doesn't support the request currency – Elias Van Ootegem Aug 22 '22 at 17:14

1 Answers1

4

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)
}
Elias Van Ootegem
  • 74,482
  • 9
  • 111
  • 149
  • Thanks for your time! I can't use **configure** with custom type like Service (maybe I entered the variable incorrectly in .env file), but I use your idea **serviceByName: map[string]*Service{}** and func **ServiceByName**. This solve me problem. Thanks a lot! – Marinso404 Aug 23 '22 at 12:39
  • @Marinso404 Why wouldn't you be able to set the environment variables? This probably could be asked as a new question. I've just taken a very cursory glance at the `configure` implementation. It's probably not the best package out there, but it was the first one I found. It seems to support fields as `map[string]string`, and assumes environment variables are JSON encoded (ie `export SERVICE_CONF="[{"name": "service1", ...}, {}]`. Nested types seem to be achieved using an interface (`EnvSetter`). You can probably handle everything that way. – Elias Van Ootegem Aug 25 '22 at 13:32
  • It might be worth posting what you've implemented for [code review](https://codereview.stackexchange.com/). It's less crowded compared to SO, but you'll get more detailed answers that will also give you some tips on how to get the most out of golang. You say you're coming from a ruby background. Just like any other language, golang and ruby have their own ways of doing things. Doing things the Ruby way in golang is almost always sub-optimal – Elias Van Ootegem Aug 25 '22 at 13:35