02使用 Go 读取配置文件

作者: 刷漆猫咪 | 来源:发表于2019-10-18 12:12 被阅读0次

    简介

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

    当时的代码中有很多硬编码的属性, 这次就要尝试从配置文件中读取.

    使用 viper 读取配置

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

    go get -u github.com/spf13/viper
    

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

    type Config struct {
        Name string
    }
    

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

    // 读取配置
    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 为空字符串的情况, 这时会从默认路径中寻找配置文件.

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

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

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

    举个例子, 当前设置的前缀为 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

    相关文章

      网友评论

        本文标题:02使用 Go 读取配置文件

        本文链接:https://www.haomeiwen.com/subject/ofsimctx.html