从 Flask 到 Gin —— 读取配置文件

文章目录

本文是 从 Flask 到 Gin 系列的第 3 篇。


配置文件是一个项目不可或缺的内容。在 MJP 的 Flask 版本中,我采用 JSON 格式的配置文件。在 Gin 的实现中,我决定保持这种格式不变。下面是一个简化了的配置文件内容。

配置文件范例

 1{
 2  "GIN_MODE": "debug",
 3  "PATH": "",
 4  "ADDRESS": "127.0.0.1:5005",
 5  "SECRET_KEY": "YutjgVSPDERGyPayXrXbwsuF_SZWiVmUw3mD4YYD_kY=",
 6  "DATABASES": {
 7    "DATABASE_URI": "zrong:123456@(127.0.0.1)/data1?charset=utf8mb4&parseTime=True&loc=Local",
 8    "DATABASE_BINDS": {
 9      "data1": "zrong:123456@(127.0.0.1)/data1?charset=utf8mb4&parseTime=True&loc=Local",
10      "data2": "zrong:123456@(127.0.0.1)/data2?charset=utf8mb4&parseTime=True&loc=Local"
11    }
12  },
13  "REGIONALS": [
14    {
15      "name": "测试服1001",
16      "r": 1001,
17      "bind_key_db": "mjptest"
18    },
19    {
20      "name": "测试服1023",
21      "r": 1023,
22      "bind_key_db": "mjptest"
23    }
24  ]
25}

上面的配置文件命名为 config.json,保存在项目根目录下。

配置模块封装:config.py

下面是 Flask 项目中,对 config.json 这个配置文件进行处理的模块。模块名称是 config.py。这个模块的主要功能有下面几个:

  1. 提供一个 getdir 模块方法,用于返回当前文件夹下相对路径的 Path 对象。
  2. 解析 config.json,将其保存到模块全局变量中,便于随时使用。这里使用标准库的 json 包完成,并封装了一个 readjson 方法。
  3. 封装了一个 getcfg 模块方法,从配置文件中获取变量。
  4. 封装了一个 getregional 模块方法,根据 REGIONAL 中的 r 值,获取这个 r 对应的配置。
  1# -*- coding: utf-8 -*-
  2"""
  3config.py
  4~~~~~~~~~~~~~~~~~~~
  5
  6初始化 app.config 中的配置
  7解析 config.json 配置文件
  8提供配置文件相关的读取和写入方法
  9"""
 10
 11import os
 12from pathlib import Path
 13import json
 14
 15
 16# getdir 使用
 17__basedir = None
 18
 19# 全局变量,用于保存 config.json 载入的配置
 20cfg_json = None
 21
 22regional_list = None
 23regional_dict = {}
 24regional_ids = []
 25
 26
 27def readjson(filename, basedir=None, throw_error=False):
 28    """ 读取一个 json 格式的配置文件
 29
 30    :param filename: 文件名
 31    :param basedir: str
 32    :param throw_error: boolean 若值为 True,则当文件不存在的时候抛出异常
 33    :returns: 解析后的 dict
 34    :rtype: dict
 35    """
 36    jsonf = getdir(filename, basedir=basedir)
 37    if jsonf.exists():
 38        return json.loads(jsonf.read_text(encoding='utf-8'), encoding='utf-8')
 39    if throw_error:
 40        raise FileNotFoundError('%s is not found!' % jsonf.resolve())
 41    return {}
 42
 43
 44def writejson(data_dict, filename, basedir=None):
 45    """ 将一个 dict 写入成为 json 文件
 46
 47    :param data_dict: 要写入的配置信息
 48    :type data_dict: dict
 49    """
 50    jsonf = getdir(filename, basedir=basedir)
 51    jsonf.write_text(json.dumps(data_dict, ensure_ascii=False, indent=2))
 52        
 53
 54def getdir(*args, basedir=None):
 55    """ 基于当前项目的运行文件夹,返回一个 pathlib.Path 对象
 56    如果传递 basedir,就基于这个 basedir 创建路径
 57    """
 58    if basedir is not None:
 59        return Path(basedir, *args)
 60    if __basedir is None:
 61        raise ValueError('please set basedir first!')
 62    return Path(__basedir, *args)
 63
 64
 65def getcfg(*args, default_value=None, data='cfg_json'):
 66    """
 67    递归获取 dict 中的值
 68    如果不提供 data,默认使用 cfg 中的值
 69    注意,getcfg 不仅可用于读取 config.yaml 的值,还可以通过传递 data 用于读取任何字典的值
 70    :param args:
 71    :param data:
 72    :return:
 73    """
 74    if data is None:
 75        return None
 76    elif data == 'cfg_json':
 77        data = cfg_json
 78    if args:
 79        if isinstance(data, dict):
 80            return getcfg(*args[1:], data=data.get(args[0], default_value))
 81        return data
 82    return data
 83
 84
 85def init_regionals():
 86    global regional_list
 87    # 从配置文件中读取 regional 的配置,存储到一个 list 和一个 dict 中
 88    regional_list = getcfg('REGIONALS')
 89    if not isinstance(regional_list, list) or len(regional_list) == 0:
 90        raise ValueError('REGIONAL is unavailable!')
 91    for regional in regional_list:
 92        r = regional.get('r')
 93        if r is None:
 94            raise KeyError('REGIONALS 配置必须包含 r key!')
 95        regional_ids.append(r)
 96        regional_dict[r] = regional
 97
 98
 99def getregional(r):
100    return regional_dict.get(r)
101
102
103def init(basedir=None, initr=True):
104    """ 初始化配置文件
105    :param initr: 是否初始化配置文件中的 initregionals
106    """
107    global __basedir, cfg_json
108    if basedir is None:
109        __basedir = os.getcwd()
110    else:
111        __basedir = basedir
112    cfg_json = readjson('config.json')
113    if initr:
114        init_regionals()

使用方法很简单,导入全局模块 config,调用其封装的方法即可:

1from mjp import config
2
3address = config.getcfg('ADDRESS')

显然,由于 Python 的动态特性,将 config.json 解析后变成一个 dict,然后处理它,是一件很灵活和轻松的事情。但 Golang 是静态语言,处理起来并没有这么方便。这里我们可以复习一下 从 Flask 到 Gin —— 处理 JSONconfig.json 需要映射成 Struct 才更容易使用。

接下来看看怎样在 Golang 中实现和 config.py 模块类似的效果。

使用 viper 读取配置

第一次看到 viper,是因为 Hugo。去年 8 月,我把博客从 Hexo 转到了 Hugo,接着坚定了学习 Golang 的念头。Hugo 使用 viper 也很正常,因为 viper 的作者 spf13 也是 Hugo 的主要作者之一。

我刚使用 Hugo 的时候,就因为它同时支持多种配置文件格式 config.yaml/config.toml/config.json 而感到惊讶(当然,uWSGI 也是支持多种配置文件格式的,所以我可能也并没有那么惊讶😄),我也是在这里第一次接触到了 TOML 格式。

viper 的优势很多(via):

  • 为各种配置项设置默认值
  • 加载并解析JSON、TOML、YAML 或 Java properties 格式的配置文件
  • 可以监视配置文件的变动、重新读取配置文件
  • 从环境变量中读取配置数据
  • 从远端配置系统中读取数据,并监视它们
  • 从命令参数中读取配置
  • 从 buffer 中读取
  • 调用函数设置配置信息

因为这些优势,我选择了 viper 作为配置文件的读取和解析库。

配置模块封装:config.go

下面是 Gin 项目中,对 config.json 这个配置文件进行处理的模块。模块名称是 config.go。这个模块的主要功能有下面几个:

  1. 提供一个 GetDir 函数,用于返回当前文件夹下的相对路径。
  2. 解析 config.json,将其内容映射到 Config 这个 Struct 中,便于随时使用。
  3. 封装了 DatabaseBind 函数,从配置文件中获取数据库绑定情况。
  4. 封装了一个 Regional 函数,根据 REGIONAL 中的 r 值,获取这个 r 对应的 Regional Struct。
  1package util
  2
  3import (
  4	"encoding/json"
  5	"fmt"
  6	"os"
  7	"path"
  8	"path/filepath"
  9	"sync"
 10
 11	"github.com/gin-gonic/gin"
 12	"github.com/spf13/viper"
 13)
 14
 15// Databases 定义配置中的 DATABASES 字段
 16type Databases struct {
 17	URI   string            `mapstructure:"DATABASE_URI"`
 18	Binds map[string]string `mapstructure:"DATABASE_BINDS"`
 19}
 20
 21// Regional 定义一个区服的配置
 22type Regional struct {
 23	Name      string `mapstructure:"name"`
 24	R         int    `mapstructure:"r"`
 25	BindKeyDb string `mapstructure:"bind_key_db"`
 26}
 27
 28// String return a string about regional
 29func (regional *Regional) String() string {
 30	return regional.Name
 31}
 32
 33/*
 34Config parse form config.json
 35*/
 36type Config struct {
 37	GinMode   string      `mapstructure:"GIN_MODE"`
 38	Path      string      `mapstructure:"PATH"`
 39	Address   string      `mapstructure:"ADDRESS"`
 40	SecretKey string      `mapstructure:"SECRET_KEY"`
 41	Databases *Databases  `mapstructure:"DATABASES"`
 42	Regionals []*Regional `mapstructure:"REGIONALS"`
 43}
 44
 45// String return the config as string
 46func (conf *Config) String() string {
 47	c := viper.AllSettings()
 48	bs, err := json.Marshal(c)
 49	if err != nil {
 50		fmt.Println(err)
 51	}
 52	return string(bs)
 53}
 54
 55// Regional return a regional in REGIONALS
 56func (conf *Config) Regional(r int) *Regional {
 57	return regionals[r]
 58}
 59
 60// DatabaseBind return a database_uri in DATABASES
 61func (conf *Config) DatabaseBind(bindKey string) string {
 62	return conf.Databases.Binds[bindKey]
 63}
 64
 65var regionals map[int]*Regional
 66var conf *Config
 67var configOnce sync.Once
 68
 69// load the config.json and return it
 70func load(content string) (*Config, error) {
 71	config := &Config{}
 72
 73	var err error
 74
 75	viper.SetConfigFile(content)
 76	err = viper.ReadInConfig()
 77	if err != nil {
 78		return nil, err
 79	}
 80
 81	err = viper.Unmarshal(&config)
 82	if err != nil {
 83		return nil, err
 84	}
 85	regionals = make(map[int]*Regional)
 86	for _, value := range config.Regionals {
 87		regionals[value.R] = value
 88	}
 89
 90	return config, nil
 91}
 92
 93// ConfigInstance is a signal Config
 94func ConfigInstance() *Config {
 95	configOnce.Do(func() {
 96		configFile := GetDir("config.json")
 97		inst, err := load(configFile)
 98		if err != nil {
 99			panic(err)
100		}
101		conf = inst
102		if conf.GinMode != "" {
103			gin.SetMode(conf.GinMode)
104		}
105	})
106	return conf
107}
108
109// GetDir return a path in pwd
110func GetDir(elem ...string) string {
111	curPath, _ := filepath.Abs(filepath.Dir(os.Args[0]))
112	args := append([]string{curPath}, elem...)
113	return path.Join(args...)
114}

上面的处理和 Python 有很大的不同。在 config.py 中,所有读入内存的配置文件数据作为一个 dict 存在,要获取其中的值,只需要使用 dict.get 并提供值的名称即可。但是在 config.go 中,我们必须把所有的值以 Struct 的形式进行预先定义和绑定。

看看怎么使用 config.go

 1package middlewares
 2
 3import (
 4	"mjp/util"
 5	"github.com/gin-gonic/gin"
 6)
 7var config *util.Config
 8var logger *util.Logger
 9
10func init() {
11    config = util.ConfigInstance()
12    logger = util.GetAppLogger()
13}
14
15func printAddress() {
16    logger.Infof("Addrss: %s", config.Address)
17}

参考


阅读系列所有文章:从 Flask 到 Gin

全文完