有条件地将 viper 配置 (toml) 解组为结构的惯用方法

Idiomatic way to conditionally unmarshal a viper config (toml) into structs

我是 Go 的新手,想知道如何以惯用的方式解决以下问题:

我正在使用 viper 将配置文件加载到程序中。我选择 toml 格式是因为我想要一个可以指定几种不同格式的所需输入的配置文件:例如,Alpha 提供程序需要一个 apikey,而 Beta 提供程序需要用户名和密码。

[alpha]
apikey = "123"
domain = "example.com"

# [beta]
# username = ""
# password = ""
# domain = ""
type ProviderService interface {                                 
    PrintDomain()                                                
}                                                                
                                                                 
type Provider struct {                                           
    Alpha `mapstructure:"alpha"`                                 
    Beta  `mapstructure:"beta"`                                                     
}                                                                
                                                                 
type Alpha struct {                                              
    Apikey string `mapstructure:"apikey"`                        
    Domain string `mapstructure:"domain"`                        
}                                                                
                                                                 
type Beta struct {                                               
    Username string `mapstructure:"username"`                    
    Password string `mapstructure:"password"`                    
    Domain   string `mapstructure:"domain"`                      
}                                                                
                                                                 
func main() {                                                    
    provider := loadConfig()                                     
    fmt.Printf("%+v\n", provider)                                
    // provider.DoAThing()  # <==== Want to do this; currently results in "ambiguous selector provider.DoAThing"                              
}                                                                
                                                                 
func (a Alpha) DoAThing() {                                   
    fmt.Println("domain", a.Domain)                              
}                                                                
func (b Beta) DoAThing() {                                    
    fmt.Println("domain", b.Domain)                              
}                                                                
                                                                 
func loadConfig() (p Provider) {                                 
    viper.AddConfigPath("./")                                    
    viper.SetConfigName("config")                                
    viper.SetConfigType("toml")                                  
                                                                 
    err := viper.ReadInConfig()                                  
    if err != nil {                                              
        panic(fmt.Errorf("Fatal error config file: %w \n", err)) 
    }                                                            
                                                                 
    err = viper.Unmarshal(&p)                                    
    if err != nil {                                              
        log.Fatal("unable to decode into struct", err)           
    }                                                            
                                                                 
    return p                                                     
}

上面的代码导致 {Alpha:{Apikey:123 Domain:example.com} Beta:{Username: Password: Domain:}},其中 empty/unused 结构仍然存在。

最终我希望 ProviderService 接口与提供者无关,这样我就可以简单地调用 provider.PrintDomain() 而不是 provider.Alpha.PrintDomain() 并让代码中充满 if/else 语句。我也愿意接受其他构建代码的方式来实现这一结果。

提前致谢!

一种可能的方法是使用 reflect

func main() {
    provider := loadConfig()
    fmt.Printf("%+v\n", provider)

    v := reflect.ValueOf(provider)
    for i := 0; i < v.NumField(); i++ {
        field := v.Field(i)
        fmt.Println("domain:", field.FieldByName("Domain"))
    }
}

它将打印 Alpha 或 Beta 结构的“域”字段。

Go Playground

这就是我最终得到的结果,它允许我根据 toml 文件中指定的内容有条件地加载结构,并且仍然使用接口将其视为结构不可知。

如果有人有任何关于将其重构为更惯用的技巧,请告诉我!

type ProviderService interface {
    DoAThing()
}

type Alpha struct {
    Apikey string `mapstructure:"apikey"`
    Domain string `mapstructure:"domain"`
}

type Beta struct {
    Username string `mapstructure:"username"`
    Password string `mapstructure:"password"`
    Domain   string `mapstructure:"domain"`
}

func main() {
    provider := loadConfig()
    if provider == nil {
        log.Fatal("unable to parse config file")
    }

    provider.DoAThing()
}

func (a Alpha) DoAThing() {
    fmt.Println("domain", a.Domain)
}
func (b Beta) DoAThing() {
    fmt.Println("domain", b.Domain)
}

func loadConfig() ProviderService {
    viper.AddConfigPath("./")
    viper.SetConfigName("config")
    viper.SetConfigType("toml")

    err := viper.ReadInConfig()
    if err != nil {
        panic(fmt.Errorf("Fatal error config file: %w \n", err))
    }

    var a Alpha
    _ = viper.UnmarshalKey("alpha", &a)
    if a != (Alpha{}) {
        return a
    }

    var b Beta
    _ = viper.UnmarshalKey("beta", &b)
    if b != (Beta{}) {
        return b
    }

    return nil
}