使用 Go 读取配置文件

简介

在上次的实践中, 启动了一个基础的 restful api server.git

当时的代码中有不少硬编码的属性, 此次就要尝试从配置文件中读取.github

使用 viper 读取配置

这里使用 viper 读取配置, 首先安装一下.web

go get -u github.com/spf13/viper
复制代码

建立一个 config 目录, 而后添加 config.go 文件, 在里面定义一个结构 Config, 使用 Name 保存配置路径.api

type Config struct {
	Name string
}
复制代码

而后定义它的两个方法, 一个读取配置, 另外一个观察配置的改动.bash

// 读取配置
func (c *Config) InitConfig() error {
	if c.Name != "" {
		viper.SetConfigFile(c.Name)
	} else {
		viper.AddConfigPath("conf")
		viper.SetConfigName("config")
	}
	viper.SetConfigType("yaml")

	// 从环境变量总读取
	viper.AutomaticEnv()
	viper.SetEnvPrefix("web")
	viper.SetEnvKeyReplacer(strings.NewReplacer("_", "."))

	return viper.ReadInConfig()
}

// 监控配置改动
func (c *Config) WatchConfig(change chan int) {
	viper.WatchConfig()
	viper.OnConfigChange(func(e fsnotify.Event) {
		log.Printf("配置已经被改变: %s", e.Name)
		change <- 1
	})
}
复制代码

读取配置时定义了多种方式, 第一个种是没有定义 Config.Name, c.Name 为空字符串的状况, 这时会从默认路径中寻找配置文件.服务器

另一种就是直接指定了配置文件的路径, 那是就直接使用这个配置文件.restful

另外, 激活了从环境变量中读取配置参数, 注意设置了全部环境变量的前缀, 前缀会自动转换为 大写_ 的格式.app

另外, 对于多层级的配置参数来讲, 会自动将环境变量中的 _ 转换为 ..函数

举个例子, 当前设置的前缀为 web. 定义一个环境变量名为 WEB_LOG_PATH, 会自动转换为 log.path, 就能够使用 viper.GetString("log.path") 或者这个环境变量对应的值了.工具

使用 Cobra 建立命令行工具

使用 viper 读取配置以后, 为了更灵活的使用, 势必要使用 CLI 工具, 以便在运行时能够指定参数等.

Cobra 是一个用于建立现代化的 CLI 界面的库, 能提供相似于 git 和 go 工具的能力.

Cobra 的做者就是建立 viper 的做者, 因此这些库都是以 🐍 命名的, viper 是蝰蛇, corba 是眼镜蛇.

corba 擅长于聚合多个命令, 它遵循 命令, 参数, 标志 的理念.

听从这种理念的模式是 APPNAME VERB NOUN --ADJECTIVE 或者 APPNAME COMMAND ARG --FLAG.

对于咱们的 web 项目来讲, 目前只有启动这个操做, 因此咱们先建立一个主动做.

建立 cmd 目录, 并建立一个名为 root.go 的文件.

var rootCmd = &cobra.Command{
	Use:   "server",
	Short: "server is a simple restful api server",
	Long: `server is a simple restful api server use help get more ifo`,
	Run: func(cmd *cobra.Command, args []string) {
		runServer()
	},
}
复制代码

主要是使用 &cobra.Command 定义一个命令.

里面的参数 Use 定义命令的名字, ShortLong 分别是短长描述, Run 定义了实际要运行的代码.

定义好主命令以后, 可能须要添加一些操做, 这些都是定义在 init() 函数中的, 同时在里面运行了 cobra.OnInitialize, 这会在每一个命令的执行阶段被运行.

// 初始化, 设置 flag 等
func init() {
	cobra.OnInitialize(initConfig)
	rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default: ./conf/config.yaml)")
}

// 初始化配置
func initConfig() {
	c := config.Config{
		Name: cfgFile,
	}

	if err := c.InitConfig(); err != nil {
		panic(err)
	}
	log.Printf("载入配置成功")
	c.WatchConfig(configChange)
}
复制代码

我在这里设置了一个名为 config 的 flag, 即配置文件对应的路径.

最后, 还须要定义一个函数, 用来包装主命令的执行:

// 包装了 rootCmd.Execute()
func Execute() {
	if err := rootCmd.Execute(); err != nil {
		log.Println(err)
		os.Exit(1)
	}
}
复制代码

如此一来, 主文件 main.go 就很是简单了, 由于咱们已经把主要的执行操做, 封装为 runServer(), 并定义在主命令之下了.

func main() {
	cmd.Execute()
}
复制代码

热重载

前面定义了一个观察 viper 配置改变的函数, 注意到它有个通道参数, 我使用通道做为消息传递机制.

// 监控配置改动
func (c *Config) WatchConfig(change chan int) {
	viper.WatchConfig()
	viper.OnConfigChange(func(e fsnotify.Event) {
		log.Printf("配置已经被改变: %s", e.Name)
		change <- 1
	})
}
复制代码

当配置文件被改变以后, 其实它自己会传递一个叫作 fsnotify.Event, 但我没有仔细研究, 而是采用了通道传递消息.

// 定义 rootCmd 命令的执行
func runServer() {
	// 设置运行模式
	gin.SetMode(viper.GetString("runmode"))

	// 初始化空的服务器
	app := gin.New()
	// 保存中间件
	middlewares := []gin.HandlerFunc{}

	// 路由
	router.Load(
		app,
		middlewares...,
	)

	go func() {
		if err := check.PingServer(); err != nil {
			log.Fatal("服务器没有响应", err)
		}
		log.Printf("服务器正常启动")
	}()

	// 服务器裕兴的地址和端口
	addr := viper.GetString("addr")
	log.Printf("启动服务器在 http address: %s", addr)

	srv := &http.Server{
		Addr:    addr,
		Handler: app,
	}
	// 启动服务
	go func() {
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatalf("listen: %s\n", err)
		}
	}()

	// 等待配置改变, 而后重启
	<-configChange
	if err := srv.Shutdown(context.Background()); err != nil {
		log.Fatal("Server Shutdown:", err)
	}
	runServer()
}
复制代码

前面都是些常规的运行启动, 包括使用一个 goroutine 检查启动的健康状态, 使用另外一个 goroutine 启动服务器.

注意最后几行, 咱们在等待通道通知配置文件已经发生了改变, 而后开始先关闭服务器, 最后从新运行启动函数.

注意: 这里可能有个 bug, 那就是修改配置文件后, OnConfigChange 会触发两次, 暂时没有什么好的解决方法. 或者能够考虑一下 github issues 上提到的 限流模式.

总结

这个过程主要研究了如何读取配置文件, 同时也使用了命令行相关的库, 便于之后扩展更多的命令.

当前部分的代码

做为版本 0.2.0

相关文章
相关标签/搜索