Golang中读取和解析/验证配置文件的惯用方法是什么
Golang中读取和解析/验证配置文件的惯用方法是什么 我想使用环境变量来设置配置。这些变量应包含有效的输入,因此我需要验证它们,但更好的做法是解析它们。(解析,而非验证)
我正在使用 Viper,并以下列代码开始:
package configuration
import "github.com/spf13/viper"
func init() {
viper.SetEnvPrefix("MY_APP")
viper.AutomaticEnv()
}
以一个日志配置为例:
package logging
type Configuration struct {
Level int `mapstructure:"LOGGING_LEVEL"`
}
我使用一个函数来读取配置:
package logging
import "github.com/spf13/viper"
func GetConfiguration() (Configuration, error) {
var configuration Configuration
err := viper.Unmarshal(&configuration)
return configuration, err
}
以及一个函数来验证它:
package logging
func ValidateConfiguration(configuration Configuration) error {
// validate configuration here
return nil
}
但有几件事我不太满意:
- 我认为更好的做法是解析这些变量
- 我该如何设置有用的回退值(/默认值)
我想我是在寻找一种描述配置模式并据此进行解析的方法。你们是如何处理配置的?
更多关于Golang中读取和解析/验证配置文件的惯用方法是什么的实战教程也可以访问 https://www.itying.com/category-94-b0.html
很抱歉进行这种无耻的自我推销,但我写了一篇关于这个问题的博客文章 🙂
A Declarative Config for Golang
简而言之,我认为目前没有一种很好的惯用方法来解决这个问题。我见过的最好方法是 kubebuilder 在结构体之上使用代码生成的方式。具体来说,他们创建了一个称为“标记”的概念,这类似于标签,但用于构建时。
这是 kubebuilder 书籍 中关于它们样子的链接:
// CronJobSpec 定义了 CronJob 的期望状态
type CronJobSpec struct {
//+kubebuilder:validation:MinLength=0
// Cron 格式的调度计划,参见 https://en.wikipedia.org/wiki/Cron。
Schedule string `json:"schedule"`
//+kubebuilder:validation:Minimum=0
// 如果作业因任何原因错过计划时间,启动作业的可选截止时间(秒)。
// 错过的作业执行将被计为失败。
// +optional
StartingDeadlineSeconds *int64 `json:"startingDeadlineSeconds,omitempty"`
更多关于Golang中读取和解析/验证配置文件的惯用方法是什么的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
这看起来相当冗长。你似乎是希望在结构体中声明式地定义验证逻辑,而不仅仅是编写代码来验证它们。是这样吗?如果我要做类似的事情,我想我可能会导入某种验证模块,或者快速自己实现一个。对于"导入现有模块"的版本,也许可以看看这个?
我认为大多数Go开发者会保持简单,编写类似这样的代码:
type Configuration struct {
Level int8 `mapstructure:"LOGGING_LEVEL"`
}
func (c Configuration) Validate() error {
minimumLogLevel := int8(zerolog.TraceLevel)
if c.Level < minimumLogLevel {
return fmt.Errorf("Log level is too low. Actual log level is %d, but the minimum log level is %d.", c.Level, minimumLogLevel)
}
maximumLogLevel := int8(zerolog.PanicLevel)
if c.Level > maximumLogLevel {
return fmt.Errorf("Log level is too high. Actual log level is %d, but the maximum log level is %d.", c.Level, maximumLogLevel)
}
return nil
}
func GetConfiguration() (Configuration, error) {
viper.SetDefault("LOGGING_LEVEL", int8(zerolog.InfoLevel))
var configuration Configuration
err := viper.Unmarshal(&configuration)
return configuration, err
}
……然后编写单元测试,以确保它的行为符合你的期望,并且将来也能继续如此。快速的编译时间意味着运行单元测试的成本很低。这段代码非常易于阅读和理解。
感谢您的回复。我尝试按照您的建议修改了代码,您能帮忙看一下吗?作为一个Go语言初学者,我需要适应地道的写法……
(实际代码被拆分到多个文件中,这里只是一个示例)
// using fmt, zerolog, viper
type LogLevelTooLowError struct {
ActualLogLevel int8
MinimumLogLevel int8
}
func (logLevelTooLowError LogLevelTooLowError) Error() string {
return fmt.Sprintf("Log level is too low. Actual log level is %d, but the minimum log level is %d.", logLevelTooLowError.ActualLogLevel, logLevelTooLowError.MinimumLogLevel)
}
// ##########################################################
type LogLevelTooHighError struct {
ActualLogLevel int8
MaximumLogLevel int8
}
func (logLevelTooHighError LogLevelTooHighError) Error() string {
return fmt.Sprintf("Log level is too high. Actual log level is %d, but the maximum log level is %d.", logLevelTooHighError.ActualLogLevel, logLevelTooHighError.MaximumLogLevel)
}
// ##########################################################
type Configuration struct {
Level int8 `mapstructure:"LOGGING_LEVEL"`
}
// ##########################################################
func GetConfiguration() (Configuration, error) {
viper.SetDefault("LOGGING_LEVEL", int8(zerolog.WarnLevel))
var configuration Configuration
err := viper.Unmarshal(&configuration)
return configuration, err
}
// ##########################################################
func ValidateConfiguration(configuration Configuration) error {
minimumLogLevel := int8(zerolog.TraceLevel)
if configuration.Level < minimumLogLevel {
return LogLevelTooLowError{
MinimumLogLevel: minimumLogLevel,
ActualLogLevel: configuration.Level,
}
}
maximumLogLevel := int8(zerolog.PanicLevel)
if configuration.Level > maximumLogLevel {
return LogLevelTooHighError{
MaximumLogLevel: maximumLogLevel,
ActualLogLevel: configuration.Level,
}
}
return nil
}
有什么建议吗?
我刚刚读了那篇文章,个人觉得它没什么价值。它主要是在争论语义问题。而且在我看来,它争论的语义是不正确的。
这两个函数几乎完全相同:它们检查提供的列表是否为空,如果为空,则用错误消息中止程序。区别完全在于返回类型:
validateNonEmpty总是返回(),即不包含任何信息的类型,但parseNonEmpty返回NonEmpty a,这是输入类型的一个细化,保留了在类型系统中获得的知识。这两个函数检查的是同一件事,但parseNonEmpty让调用者能够访问它学到的信息,而validateNonEmpty只是将其丢弃。
你无法“解析”某个东西不为空,但你可以“验证”它不为空或“断言”它不为空。你无法“解析”某个东西看起来像有效的电子邮件地址,但你可以“验证”/“核实”/“断言”它看起来像有效的电子邮件地址。同样地,仅仅因为某个东西被称为“验证”,并不意味着它不能同时返回信息和沿途发现的任何错误。
但有几件事我不喜欢
- 我认为最好解析变量
你为什么这么认为?你能在你的具体案例中给我一个例子,说明“解析”比“验证”更可取吗?在你的具体情况下,你试图避免哪些类型的错误/问题?
- 我如何设置有用的回退值(/默认值)
我对 Viper 没有太多经验,但你可以设置默认值。
我想我正在寻找一种方法来描述配置模式并据此进行解析。你如何处理你的配置?
从高层次上讲,这取决于我的目标。对于云/容器化应用,我更喜欢环境变量。对于更接近硬件/没有容器的本地开发,我更喜欢配置文件(我默认使用 JSON,因为它非常普遍,但许多 Go 开发者出于某种原因喜欢 toml)。但无论我的配置来自哪里,我处理它的方式都是一样的:
- 尝试读取配置。这包括诸如在环境变量未设置时回退到配置文件等操作。这包括诸如解析字符串以确保它们是目标所需的任何数据类型(在我看来,这与验证它们不同)。
- 尝试验证配置,使其达到我基本满意的状态,即应用程序至少有机会运行(例如,如果我需要一个 SQL 连接字符串而它是空的,我知道应用程序无法运行;如果日志级别未设置,我可以直接使用某个默认值)。
- 如果我知道应用程序无法运行,就向控制台
log.Fatalf一些有用的信息。这样做的目的是,当 Google Cloud Run 无法启动时,你希望可以检查日志,看到你忘记使用密钥管理器来设置 APP_CONNECTION_STRING 环境变量之类的信息。再次强调,尽量提供有用/具体的信息。
老实说,我通常只使用标准库。如果你真的想,你可以用结构体标签来定义配置的元数据:
type appConfig struct {
LogLevel int `json:"logLevel" env:"MY_APP_LOG_LEVEL" default:"0" desc:"Can be set to 0, 1, 2."`
Env string `json:"env" env:"MY_APP_ENVIRONMENT" default:"Dev" desc:"Dev, Test, or Production."`
}
… 然后使用反射来读取它们:
func main() {
conf := appConfig{}
printStructTags(reflect.ValueOf(conf))
}
func printStructTags(f reflect.Value) {
// Iterate over our fields and grab tags
for i := 0; i < f.NumField(); i++ {
field := f.Type().Field(i)
tag := field.Tag
fmt.Printf("Field: %v.\n\tExpected JSON tag: %v.\n\tEnvironment variable: %v.\n\tDefault: %v.\n\tDescription: %v.\n", field.Name, tag.Get("json"), tag.Get("env"), tag.Get("default"), tag.Get("desc"))
}
}
Field: LogLevel.
Expected JSON tag: logLevel.
Environment variable: MY_APP_LOG_LEVEL.
Default: 0.
Description: Can be set to 0, 1, 2..
Field: Env.
Expected JSON tag: env.
Environment variable: MY_APP_ENVIRONMENT.
Default: Dev.
Description: Dev, Test, or Production..
目前,我建议你坚持使用 Viper 的示例。Viper 已经在许多生产项目中成功使用,包括 Hugo。我敢打赌它能很好地满足你的需求。如果不能,配置是一个相对简单的主题,你可以直接使用标准库。如前所述,你可以使用结构体标签来构建自己的配置对象,并将元数据嵌入结构体本身。


