Golang – 通过名称动态访问结构体属性

huangapple go评论88阅读模式
英文:

Golang - Access to struct property dynamically by name

问题

我有一个配置结构体,简化版本如下:

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"),
				},
			},
		},
	}
}

我尝试通过变量来访问服务的 CRC 或服务卡的 CRC,代码如下:

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

cfg := config.Cfg()

crc := cfg[variable].cards[currency] // 这样是不起作用的

我一直尝试使用 map,代码如下:

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"]) // 这样是可以的
}

但是对我来说看起来很奇怪。你知道更好的编写配置的方法吗?可以使用结构体吗?我来自 Ruby 行星,对于我来说 Golang 是新的。

编辑:

我从 Rabbit 队列接收消息,根据这些消息创建支付。不幸的是,不同的支付方式需要“自己”的授权(CRC 和 MerchantId)。调用如下所示:

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,
})

有没有关于如何正确处理的想法?

英文:

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?

答案1

得分: 4

忽略reflect包,简单来说:你不能这样做。你不能通过字符串变量动态访问结构体字段。但是你可以在map上使用变量,因为在map中访问数据是通过哈希表查找实现的。而结构体不是。

我要再次强调我的评论的主要观点:你似乎正在尝试使用环境变量来设置配置结构体的值。这个问题已经有了很好的解决方案。我们多年来一直在做这个。我进行了快速的谷歌搜索,找到了这个仓库,它正好做了你想做的事情(还有更多):configure

使用这个包,你可以像这样声明你的配置结构体:

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
}

然后,从环境变量加载配置:

func LoadEnv() (*Config, error) {
    c := Config{
         serviceByName: map[string]*Service{},
    } // 如果需要,设置默认值
    if err := configure.ParseEnv(&c); err != nil {
        return nil, err
    }
    // 初始化方便使用的字段,比如serviceByName:
    for _, svc := range c.Services {
        c.serviceByName[svc.Name] = svc
    }
    return &c, nil
}

ServiceByName函数返回给定服务的配置的副本:

func (c Config) ServiceByName(n string) (Service, error) {
    s, ok := c.serviceByName[n]
    if !ok {
        return nil, errors.New("service with given name does not exist")
    }
    return *s, nil
}

你还可以定义一个单独的Load函数,它会优先考虑一种类型的配置。通过这些标签,我们支持环境变量、Yaml文件和命令行参数。通常情况下,命令行参数会覆盖其他格式的配置。至于Yaml和环境变量,你可以从两个方面进行讨论:像ENVIRONMENT这样的环境变量并不是很具体,可能会被错误地多个进程使用。然而,如果你正确部署,这不应该是个问题,所以出于这个原因,我会优先考虑环境变量而不是Yaml文件:

func Load(args []string) (*Config, error) {
    c := &Config{
        Environment:   "devel", // 默认值
        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.ParseCommandLine(c, args); err != nil {
            return nil, err
        }
    }
    // 初始化方便使用的字段,比如serviceByName:
    for _, svc := range c.Services {
        c.serviceByName[svc.Name] = svc
    }
    return c, nil
}

然后在你的主包中:

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)
}
英文:

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)
}

huangapple
  • 本文由 发表于 2022年8月22日 23:20:12
  • 转载请务必保留本文链接:https://go.coder-hub.com/73447461.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定