Go语言微服务框架 - 2.实现加载静态配置文件

简介: 今天,我们先将重点放到加载配置文件库的技术选型,顺便分享一些常见的问题。

我们的基础RPC服务已经正常运行,我们再来看下一个特性:配置文件的加载。

首先,我们要正确地认识到配置文件的重要性:在程序交付后,变更代码的成本很大;相对而言,变更配置文件的成本就比较小。但有的同学又走了另一个极端,也就是将大量的逻辑放入到配置文件中,导致配置文件膨胀,本质上就是将部分本应在代码中维护的内容转移到了配置文件,导致配置文件也很难维护。

今天,我们先将重点放到加载配置文件库的技术选型,顺便分享一些常见的问题。

一个基础的加载配置文件示例

Go语言中,用官方库就能快速实现配置文件的加载,下面就是一个简单的代码实现:

b, err := ioutil.ReadFile("config.json")
if err != nil {
   
  panic(err)
}
var config MyConfig
err = json.Unmarshal(b, &config)
if err != nil {
   
  panic(err)
}

关键的实现分为两块:

  1. 读取文件中的数据
  2. 将数据解析到Go程序的对象中,作为可识别的数据结构,这里指定了数据类型为json

v0.2.0:实现加载静态配置文件

项目链接 https://github.com/Junedayday/micro_web_service/tree/v0.2.0

目标

从配置文件中解析数据到程序中,并具备更高的可读性和扩展性。

关键技术点

  1. 命令行参数与配置文件的差异
  2. github.com/spf13/viper的介绍
  3. 使用viper库的推荐方式

目录构造

github.com/spf13/viper中存在一个全局变量var v *Viper点击查看),如果我们调用默认的viper包,其实就是将参数解析到这个全局变量中。

在具体的项目中,更推荐的方式是将这个变量保存到内部项目中,作为一个项目中的全局变量,所以我们会新建一个viper.New()。配置参数会被全局调用,为了保证不会发生循环依赖,我们就需要一个专门的package来保存这个全局变量,这里对应项目中的internal/config/viper.go

--- micro_web_service            项目目录
    |-- gen                            从idl文件夹中生成的文件,不可手动修改
       |-- idl                             对应idl文件夹
          |-- demo                             对应idl/demo服务
             |-- demo.pb.go                        demo.proto的基础结构
             |-- demo.pb.gw.go                     demo.proto的HTTP接口,对应gRPC-Gateway
             |-- demo_grpc.pb.go                   demo.proto的gRPC接口代码
    |-- idl                            原始的idl定义
       |-- demo                            业务package定义
          |-- demo.proto                       protobuffer的原始定义
    |-- internal                       项目的内部代码,不对外暴露
       |-- server                          服务器的实现
          |-- demo.go                          server中对demo这个服务的接口实现
          |-- server.go                        server的定义,须实现对应服务的方法
       |-- config                          新增:配置相关的文件夹
          |-- viper.go                         新增:viper的相关加载逻辑
    |-- buf.gen.yaml                   buf生成代码的定义
    |-- buf.yaml                       buf工具安装所需的工具
    |-- gen.sh                         buf生成的shell脚本
    |-- go.mod                         Go Module文件
    |-- main.go                        项目启动的main函数

1.命令行参数与配置文件的差异

命令行参数类似于./demo --config=a.yaml --http_port=8080 --grpc_port=8081Go语言中自带flag包可供解析,开源的有pflag和相关的扩展工具,如cobra

而配置文件则是将参数从文件解析到内存中,一般用读取文件+反序列化工具来使用。

同样是解析参数到程序里,我们该选择哪种方案呢?我们从可读性和可维护性来对比下:

  • 可读性:命令行参数是扁平化的,可读性远不如格式化后的配置文件
  • 可维护性:配置文件增加了一个维护项,但成本不高

所以,我个人倾向于的方案是:

  • 命令行参数:用于维护极少量关键性的参数,如配置文件的路径
  • 配置文件:维护绝大多数的参数

在某些极端的场景中,比如提供一个纯二进制文件作为工具,那不得不把所有配置参数都放入到命令行参数中。这并不是我们微服务框架要讨论的场景。所以,接下来我们重点讨论配置文件的加载。

关于pflag相关的内容,在后续程序复杂到一定阶段后会引入。

2.github.com/spf13/viper的介绍

对比上面的方案,我们来看一个业内使用最广的Go语言配置加载库github.com/spf13/viper的实现,对应的代码如下:

viper.SetConfigName("config")        // config file name without file type
viper.SetConfigType("yaml")          // config file type
viper.AddConfigPath("./")            // config file path
if err := viper.ReadInConfig(); err != nil {
   
  panic(err)
}

而在获取配置文件时,又采用key-value形式的语法:

viper.GetInt("server.grpc.port")

详细的特性我会在Go语言技巧系列里说明,今天我们聚焦于工程侧的宏观特性,来聊聊这个库的优劣势:

可选的参数序列化

viper库的源码中(点击跳转),我们可以看到它支持多种本地文件类型远程k-v数据库

// SupportedExts are universally supported extensions.
var SupportedExts = []string{
   "json", "toml", "yaml", "yml", "properties", "props", "prop", "hcl", "dotenv", "env", "ini"}

// SupportedRemoteProviders are universally supported remote providers.
var SupportedRemoteProviders = []string{
   "etcd", "consul", "firestore"}

我们先忽略远程的存储,先看一下最常用的几个序列化的库:

  1. JSON: 官方自带的encoding/json
  2. TOML: 开源的github.com/pelletier/go-toml
  3. YAML: 官方推荐的gopkg.in/yaml.v2
  4. INI:官方推荐的gopkg.in/ini.v1

在这四种技术选型时,我个人倾向于选择JSONYAML。进一步斟酌时,虽然JSON的适用范围最广,但当配置文件复杂到一定程度时,JSON格式的配置文件很难通过换行来约束,当存在大量的嵌套时,可读性很差。所以,我个人比较推荐使用YAML格式的配置文件,一方面通过强制的换行约束,可读性很棒;另一方面云原生相关技术大量使用了YAML作为配置文件,尤其是Kubernetes中各种定义。

例如,我们将服务的端口改造到配置文件里,就成了:

server:
  http:
    port: 8081
  grpc:
    port: 8082

对应的Go语言代码为:

viper.GetInt("server.http.port")
viper.GetInt("server.grpc.port")

可扩展的获取参数方法

viper库提供的获取参数方式为viper.Get{type}("{level1}.{level2}.{level3}...")的格式。随着配置文件的扩大,也只需新增Get方法即可。

从获取参数的方法来看,它的设计分为3种:

  1. 基本类型,直接提供Get{具体类型}的方法,如GetIntGetString
  2. 任意类型,提供Get(key string) interface{},自行转化
  3. 复杂类型的反序列化,提供UnmarshalKey等方法,更方便地获取复杂结构

我个人建议各位只使用第一类的方法,将配置文件这个模块做到最简化。毕竟,配置文件的复杂化很容易引入各种问题,占用大量的排查故障的时间。如果你的系统必须引入一套非常复杂的配置,那么我更建议将它独立成一个专门的服务,用来维护这套配置。

3.使用viper库的推荐方式

如果你仔细地阅读viper相关的代码,你会发现它有很多超酷的特性。但今天,我想告诉各位:请克制地使用进阶的特性,最棒的特性往往是简洁的

我们对照着官方的README文件中介绍的特性进行讲解。

尽量避免手动设置的参数值

原文链接

viper.SetDefault函数可以给某些参数设置默认值,如果只是少数的几个参数还是很容易维护的。但如果设置的值过多了,就会给阅读代码的人带来困扰:这个参数是来自配置文件,还是在代码某处手动设置的?也就是存在二义性,增加了排查问题的复杂度。

明确配置文件的来源

原文链接

viper.AddConfigPath("/etc/appname/")   // path to look for the config file in
viper.AddConfigPath("$HOME/.appname")  // call multiple times to add many search paths
viper.AddConfigPath(".")               // optionally look for config in the working directory

viper支持多个配置文件的路径,这虽然带来了便利性,但如果多个文件路径中都存在配置文件,那究竟以哪个为准?这也是一个二义性的问题,所以我个人更建议只设置一个,而这个路径由flag传入。

静态配置与动态配置的分离

原文链接

viper提供了接口viper.WatchConfig(),可以监听文件的变化,然后做相应的调整。这个特性很酷,我们可以用它实现热加载。但这个特性很容易让人产生混淆:例如发生了某个BUG,如何确定当时的配置文件情况?其实,这就需要引入一定的版本管理机制。

我更建议采用静态配置和动态配置分离的方案,也就是配置文件负责静态的、固定的配置,一旦启动后只加载一次;而动态的配置放在带版本管理的配置中心里,具备热加载的特性。

所以,我不太建议在配置文件这里引入监听文件变化的特性。

核心代码示例

main.go

flag中解析出配置文件路径,传到config包中用于解析。

var configFilePath = flag.String("c", "./", "config file path")
flag.Parse()

if err := config.Load(*configFilePath); err != nil {
   
  panic(err)
}

internal/config/viper.go

加载的代码并不多,尽量保证配置信息的简洁易懂。

// 全局Viper变量
var Viper = viper.New()

func Load(configFilePath string) error {
   
    Viper.SetConfigName("config")       // config file name without file type
    Viper.SetConfigType("yaml")         // config file type
    Viper.AddConfigPath(configFilePath) // config file path
    return Viper.ReadInConfig()
}

配置使用方

config.Viper.GetInt("server.grpc.port")

使用viper库的注意事项

在使用viper获取配置时,我们需要手动组装key,也就是"{level1}.{level2}.{level3}..."这种形式。这时,我们只能对照着原始配置文件逐个填充字段,一不小心填错、就会发生奇怪的问题。而如果采用的是将配置文件解析到结构体的方法,就能很容易地避免这个问题。

考虑到扩展性,官方库推荐的是手动组装key的方式,所以需要大家在认真查看这个key是否有效。

总结

加载静态配置文件是一个很常见的功能,viper提供了一套完整方案,兼具简洁和扩展性;与此同时,我们要学会克制,不要看到了viper中提供的各种特性、就想着应用到实际项目中,也就是常说的:手里拿了个锤子,看啥都是钉子

目录
相关文章
|
16小时前
|
存储 缓存 安全
Golang深入浅出之-Go语言中的并发安全容器:sync.Map与sync.Pool
Go语言中的`sync.Map`和`sync.Pool`是并发安全的容器。`sync.Map`提供并发安全的键值对存储,适合快速读取和少写入的情况。注意不要直接遍历Map,应使用`Range`方法。`sync.Pool`是对象池,用于缓存可重用对象,减少内存分配。使用时需注意对象生命周期管理和容量控制。在多goroutine环境下,这两个容器能提高性能和稳定性,但需根据场景谨慎使用,避免不当操作导致的问题。
13 4
|
1天前
|
安全 Go 开发者
Golang深入浅出之-Go语言中的CSP模型:深入理解并发哲学
【5月更文挑战第1天】Go语言基于CSP理论,借助goroutines和channels实现独特的并发模型。Goroutine是轻量级线程,通过`go`关键字启动,而channels提供安全的通信机制。文章讨论了数据竞争、死锁和goroutine泄漏等问题及其避免方法,并提供了一个生产者消费者模型的代码示例。理解CSP和妥善处理并发问题对于编写高效、可靠的Go程序至关重要。
8 2
|
1天前
|
设计模式 Go 调度
Golang深入浅出之-Go语言中的并发模式:Pipeline、Worker Pool等
【5月更文挑战第1天】Go语言并发模拟能力强大,Pipeline和Worker Pool是常用设计模式。Pipeline通过多阶段处理实现高效并行,常见问题包括数据竞争和死锁,可借助通道和`select`避免。Worker Pool控制并发数,防止资源消耗,需注意任务分配不均和goroutine泄露,使用缓冲通道和`sync.WaitGroup`解决。理解和实践这些模式是提升Go并发性能的关键。
13 2
|
1天前
|
JSON 监控 安全
Golang深入浅出之-Go语言中的反射(reflect):原理与实战应用
【5月更文挑战第1天】Go语言的反射允许运行时检查和修改结构,主要通过`reflect`包的`Type`和`Value`实现。然而,滥用反射可能导致代码复杂和性能下降。要安全使用,应注意避免过度使用,始终进行类型检查,并尊重封装。反射的应用包括动态接口实现、JSON序列化和元编程。理解反射原理并谨慎使用是关键,应尽量保持代码静态类型。
11 2
|
1天前
|
Go
Golang深入浅出之-Go语言代码质量与规范:遵循Gofmt与Linting
【5月更文挑战第1天】本文讨论了如何使用`gofmt`和Lint工具提升Go代码质量。`gofmt`负责自动格式化代码,保持风格统一,而Lint工具如`golint`、`govet`、`staticcheck`则进行静态分析,检查潜在错误和未使用的变量。通过集成`gofmt`检查到CI/CD流程,避免格式冲突,并使用Lint工具发现并修复问题,如未处理的错误、不规范命名。遵循这些最佳实践,可提高代码可读性、团队协作效率和可维护性。
9 3
|
2天前
|
JSON 安全 Java
2024年的选择:为什么Go可能是理想的后端语言
【4月更文挑战第27天】Go语言在2024年成为后端开发的热门选择,其简洁设计、内置并发原语和强大工具链备受青睐。文章探讨了Go的设计哲学,如静态类型、垃圾回收和CSP并发模型,并介绍了使用Gin和Echo框架构建Web服务。Go的并发通过goroutines和channels实现,静态类型确保代码稳定性和安全性,快速编译速度利于迭代。Go广泛应用在云计算、微服务等领域,拥有丰富的生态系统和活跃社区,适合作为应对未来技术趋势的语言。
8 0
|
2天前
|
Go 开发者
Golang深入浅出之-Go语言项目构建工具:Makefile与go build
【4月更文挑战第27天】本文探讨了Go语言项目的构建方法,包括`go build`基本命令行工具和更灵活的`Makefile`自动化脚本。`go build`适合简单项目,能直接编译Go源码,但依赖管理可能混乱。通过设置`GOOS`和`GOARCH`可进行跨平台编译。`Makefile`适用于复杂构建流程,能定义多步骤任务,但编写较复杂。在选择构建方式时,应根据项目需求权衡,从`go build`起步,逐渐过渡到Makefile以实现更高效自动化。
12 2
|
2天前
|
存储 Go
Golang深入浅出之-Go语言依赖管理:GOPATH与Go Modules
【4月更文挑战第27天】Go语言依赖管理从`GOPATH`进化到Go Modules。`GOPATH`时代,项目结构混乱,可通过设置多个工作空间管理。Go Modules自Go 1.11起提供更现代的管理方式,通过`go.mod`文件控制依赖。常见问题包括忘记更新`go.mod`、处理本地依赖和模块私有化,可使用`go mod tidy`、`replace`语句和`go mod vendor`解决。理解并掌握Go Modules对现代Go开发至关重要。
11 2
|
2天前
|
安全 测试技术 Go
Golang深入浅出之-Go语言单元测试与基准测试:testing包详解
【4月更文挑战第27天】Go语言的`testing`包是单元测试和基准测试的核心,简化了测试流程并鼓励编写高质量测试代码。本文介绍了测试文件命名规范、常用断言方法,以及如何进行基准测试。同时,讨论了测试中常见的问题,如状态干扰、并发同步、依赖外部服务和测试覆盖率低,并提出了相应的避免策略,包括使用`t.Cleanup`、`t.Parallel()`、模拟对象和检查覆盖率。良好的测试实践能提升代码质量和项目稳定性。
8 1
|
2天前
|
运维 监控 Go
Golang深入浅出之-Go语言中的日志记录:log与logrus库
【4月更文挑战第27天】本文比较了Go语言中标准库`log`与第三方库`logrus`的日志功能。`log`简单但不支持日志级别配置和多样化格式,而`logrus`提供更丰富的功能,如日志级别控制、自定义格式和钩子。文章指出了使用`logrus`时可能遇到的问题,如全局logger滥用、日志级别设置不当和过度依赖字段,并给出了避免错误的建议,强调理解日志级别、合理利用结构化日志、模块化日志管理和定期审查日志配置的重要性。通过这些实践,开发者能提高应用监控和故障排查能力。
8 1