在 Go 应用程序中传递配置的简洁方法
清洁环境
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
这篇文章转载自我的博客kaznacheev.me。欢迎新读者,我很乐意与大家讨论我文章的内容。
如今,组织应用程序配置的方法有很多种,而且这些方法都可以同时使用。
传统.ini文件、.json配置文件.toml、.yaml现代配置文件,.env当然还有容器环境。别忘了还有命令行参数!我漏掉了什么吗?
说实话,我非常讨厌接口中的任何隐含部分。命令行界面(CLI)当然也一样。你的任何接口,无论是公共接口还是内部接口,API接口还是对象接口,类方法还是模块外观——它们都必须公平协作。你和对方之间的约定应该是明确的、合法的,而不是在页面底部用小字标注的。
这意味着,他们必须严格按照要求收取费用,并严格按照承诺交付。他们不应该在你转身离开时从你口袋里拿走任何东西。如果你同意立即交易,他们不应该在一两周后才发货。他们可以保留内部信息。并非所有信息都应该是不可更改的。但你始终应该能够进行清晰、公平的互动,这意味着所有互动都必须是可见且合法的,也就是说,你能够将数据逐一传递。否则,你将丢失合同细节,迟早会被欺骗。
任何接口都必须只有一个入口点,这意味着你需要一次性显式传递接口所需的所有数据。数据可以是任何类型的——值、结构化或序列化数据、待解析的文件路径或要连接的套接字——关键在于将数据放在接口调用处。
清理配置的方法
所以,现在这种把所有有意义的设置都按照十二要素应用理论放到环境变量里的趋势,在我看来很糟糕。不,十二要素应用的理念很好。但我不得不通过环境变量(也就是全局变量)来隐式地传递配置,这让我很沮丧。
真是可惜,一开始我把所有配置都放在一个配置文件里,然后为每个环境(例如本地、测试、生产等)创建单独的配置文件。通常配置文件的结构都很好,我可以根据参数的用途进行分组。但这其实并不方便,因为我需要在代码仓库中为每个环境存储多个配置文件,并且在配置结构发生变化后需要更新每个配置文件。
后来我改用命令行参数来配置,因为我需要直接向应用程序传递一些数据,例如密钥、本地路径和其他可变数据,以及一些无法保存在配置文件中的数据。这样做的好处是,现在我可以传递一些在不同环境下会变化的值作为参数,并为每个系统使用同一个配置文件。而且效果一直很好,直到……
容器技术已经出现。而设置容器环境的常用方法是使用环境变量。理论上,可以将配置文件存储在密钥中,但这完全没有用处。
环境变量的主要缺点在于其隐式特性。你无法仅通过查看应用程序help输出或配置文件结构来了解变量集及其结构。当应用程序配置基于环境变量时,你必须为其编写文档,并且需要承担更新和同步的负担。否则,用户将无法获取配置信息,从而严重影响他们的使用体验。
调试这类应用程序也很困难。作为全局变量,环境变量会隐式地发挥作用,随时可能影响程序的行为。当代码足够小,可以装进内存时,这没什么大不了的;但随着代码量的增长,你会很难记住某个配置值是从哪里获取的。
但我最终还是找到了一个我认为可行且符合我要求的方案。它结合了几种技术,所以让我快速概述一下。
配置文件
首先,我们仍然需要使用配置文件。我使用它有几个目的:
- 声明配置结构;
- 保留每个变量的默认值;
- 文档变量或章节;
- 为应用程序用户提供配置示例;
我更喜欢将配置信息存储在 YAML 文件中。我不想引发争论,只是想说明为什么对我来说这是最佳选择:
- 层级结构——我可以在灵活的组织结构中保持高度的灵活性;
- 简洁的标记——我不需要任何括号或一堆符号来与解析器通信;
- 备注——我可以提供详细的文档、选项、局限性、示例、最佳实践等;
- 丰富的语义——一些非常高级的技术,例如锚点、别名、扩展、嵌入等等。我并不经常使用它们,但有时它们非常有用;
假设我们有一个简单的配置文件,如下所示:
# Server configurations
server:
host: "localhost"
port: 8000
# Database credentials
database:
user: "admin"
pass: "super-pedro-1980"
要在 Go 中使用.yml文件中的数据,需要像处理 JSON 数据一样将其反序列化为 Go 结构。
映射关系看起来类似于 JSON:
type Config struct {
Server struct {
Port string `yaml:"port"`
Host string `yaml:"host"`
} `yaml:"server"`
Database struct {
Username string `yaml:"user"`
Password string `yaml:"pass"`
} `yaml:"database"`
}
我使用gopkg.in/yaml.v2Canonical 的库来解析 YAML 文件。
你可以用它yml.Unmarshal来解析字节切片,但在大多数情况下,你会使用一些作为io.Reader实现提供的数据,所以我使用的是读取字节流而不是存储在内存中的完整数据的解码器:
f, err := os.Open("config.yml")
if err != nil {
processError(err)
}
defer f.Close()
var cfg Config
decoder := yaml.NewDecoder(f)
err = decoder.Decode(&cfg)
if err != nil {
processError(err)
}
就是这样。就像任何 JSON 文件一样。现在你可以继续编写你自己的、文档齐全、结构清晰的配置文件,它也可以作为你代码仓库中的示例。
环境变量
现在我们来谈谈环境变量。通常情况下,它们分散在应用程序的各个角落。但还有另一种方法。在 Go 语言中,你可以像对 JSON、YAML 等格式一样,将环境变量分配给结构体字段。代码看起来会是这样的:
type Config struct {
Server struct {
Port string `envconfig:"SERVER_PORT"`
Host string `envconfig:"SERVER_HOST"`
}
Database struct {
Username string `envconfig:"DB_USERNAME"`
Password string `envconfig:"DB_PASSWORD"`
}
}
魔法就藏在github.com/kelseyhightower/envconfig图书馆里。
只需几行代码,即可检索环境变量并将它们分配给您定义的结构字段:
var cfg Config
err := envconfig.Process("", &cfg)
if err != nil {
processError(err)
}
就这样。现在所有环境变量都集中在一个地方了。无需浏览整个代码库来查找变量的使用位置,只需追踪配置结构即可,它只有一个入口点。
此外,现在所有环境变量都已在同一位置声明。您可以打开配置结构,查看应用程序所需的所有环境变量的完整列表。该库提供了一系列Usage具有丰富输出功能的函数,允许您将环境变量列表添加到辅助输出或其他任何您想要的位置。您的应用程序用户会对此表示赞赏。
现在你可以用你喜欢的方式来提供环境变量——.env 文件、容器设置、Makefile 文件或者 shell 脚本。这完全取决于你。
总而言之
现在让我们把它们混合起来!
相同的结构看起来会像这样:
type Config struct {
Server struct {
Port string `yaml:"port", envconfig:"SERVER_PORT"`
Host string `yaml:"host", envconfig:"SERVER_HOST"`
} `yaml:"server"`
Database struct {
Username string `yaml:"user", envconfig:"DB_USERNAME"`
Password string `yaml:"pass", envconfig:"DB_PASSWORD"`
} `yaml:"database"`
}
首先,我从 YAML 文件加载数据。它还可以作为一组默认值。
然后我加载环境变量并覆盖已填充的字段。也就是说,您无需处理缺失值,它们会从配置文件中自动填充。
func main() {
var cfg Config
readFile(&cfg)
readEnv(&cfg)
fmt.Printf("%+v", cfg)
}
func processError(err error) {
fmt.Println(err)
os.Exit(2)
}
func readFile(cfg *Config) {
f, err := os.Open("config.yml")
if err != nil {
processError(err)
}
defer f.Close()
decoder := yaml.NewDecoder(f)
err = decoder.Decode(cfg)
if err != nil {
processError(err)
}
}
func readEnv(cfg *Config) {
err := envconfig.Process("", cfg)
if err != nil {
processError(err)
}
}
因此,这种方法有很多优点:
- 单一入口点 - 易于查找每个配置变量的来源;
- 通过标签进行简单声明;
- 结构化配置 - 您可以根据用途对配置部分进行分组;
- 在配置文件中声明默认值,并根据每个环境的需要进行覆盖;
- 应用程序中使用的环境变量的明确列表;
我不确定这种方法是否涵盖所有可能的应用场景,但它非常实用,最重要的是它很明确。
更新:我已经将所有讨论过的技术添加到一个名为 CleanEnv 的小型简单配置包中。

清洁环境
极简配置读取器

概述
这是一个简单的配置读取工具,它的功能仅限于以下几点:
- 从文件中读取并解析配置结构
- 从环境变量读取和覆盖配置结构
- 生成详细的变量列表以帮助输出
内容
安装
要安装软件包,请运行
go get -u github.com/ilyakaznacheev/cleanenv
用法
该软件包的设计理念是简单易用、清晰明了。
主要思路是使用结构化的配置变量,而不是像某些库那样使用任何类型的动态配置字段集,以避免不必要的类型转换,并将配置作为简单的结构而不是具有复杂行为的对象在程序中传递。
你只需采取以下几个步骤……
文章来源:https://dev.to/ilyakaznacheev/a-clean-way-to-pass-configs-in-a-go-application-1g64