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


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

配置文件范例

{
  "GIN_MODE": "debug",
  "PATH": "",
  "ADDRESS": "127.0.0.1:5005",
  "SECRET_KEY": "YutjgVSPDERGyPayXrXbwsuF_SZWiVmUw3mD4YYD_kY=",
  "DATABASES": {
    "DATABASE_URI": "zrong:123456@(127.0.0.1)/data1?charset=utf8mb4&parseTime=True&loc=Local",
    "DATABASE_BINDS": {
      "data1": "zrong:123456@(127.0.0.1)/data1?charset=utf8mb4&parseTime=True&loc=Local",
      "data2": "zrong:123456@(127.0.0.1)/data2?charset=utf8mb4&parseTime=True&loc=Local"
    }
  },
  "REGIONALS": [
    {
      "name": "测试服1001",
      "r": 1001,
      "bind_key_db": "mjptest"
    },
    {
      "name": "测试服1023",
      "r": 1023,
      "bind_key_db": "mjptest"
    }
  ]
}

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

配置模块封装:config.py

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

  1. 提供一个 getdir 模块方法,用于返回当前文件夹下相对路径的 Path 对象。
  2. 解析 config.json,将其保存到模块全局变量中,便于随时使用。这里使用标准库的 json 包完成,并封装了一个 readjson 方法。
  3. 封装了一个 getcfg 模块方法,从配置文件中获取变量。
  4. 封装了一个 getregional 模块方法,根据 REGIONAL 中的 r 值,获取这个 r 对应的配置。
# -*- coding: utf-8 -*-
"""
config.py
~~~~~~~~~~~~~~~~~~~

初始化 app.config 中的配置
解析 config.json 配置文件
提供配置文件相关的读取和写入方法
"""

import os
from pathlib import Path
import json


# getdir 使用
__basedir = None

# 全局变量,用于保存 config.json 载入的配置
cfg_json = None

regional_list = None
regional_dict = {}
regional_ids = []


def readjson(filename, basedir=None, throw_error=False):
    """ 读取一个 json 格式的配置文件

    :param filename: 文件名
    :param basedir: str
    :param throw_error: boolean 若值为 True,则当文件不存在的时候抛出异常
    :returns: 解析后的 dict
    :rtype: dict
    """
    jsonf = getdir(filename, basedir=basedir)
    if jsonf.exists():
        return json.loads(jsonf.read_text(encoding='utf-8'), encoding='utf-8')
    if throw_error:
        raise FileNotFoundError('%s is not found!' % jsonf.resolve())
    return {}


def writejson(data_dict, filename, basedir=None):
    """ 将一个 dict 写入成为 json 文件

    :param data_dict: 要写入的配置信息
    :type data_dict: dict
    """
    jsonf = getdir(filename, basedir=basedir)
    jsonf.write_text(json.dumps(data_dict, ensure_ascii=False, indent=2))
        

def getdir(*args, basedir=None):
    """ 基于当前项目的运行文件夹,返回一个 pathlib.Path 对象
    如果传递 basedir,就基于这个 basedir 创建路径
    """
    if basedir is not None:
        return Path(basedir, *args)
    if __basedir is None:
        raise ValueError('please set basedir first!')
    return Path(__basedir, *args)


def getcfg(*args, default_value=None, data='cfg_json'):
    """
    递归获取 dict 中的值
    如果不提供 data,默认使用 cfg 中的值
    注意,getcfg 不仅可用于读取 config.yaml 的值,还可以通过传递 data 用于读取任何字典的值
    :param args:
    :param data:
    :return:
    """
    if data is None:
        return None
    elif data == 'cfg_json':
        data = cfg_json
    if args:
        if isinstance(data, dict):
            return getcfg(*args[1:], data=data.get(args[0], default_value))
        return data
    return data


def init_regionals():
    global regional_list
    # 从配置文件中读取 regional 的配置,存储到一个 list 和一个 dict 中
    regional_list = getcfg('REGIONALS')
    if not isinstance(regional_list, list) or len(regional_list) == 0:
        raise ValueError('REGIONAL is unavailable!')
    for regional in regional_list:
        r = regional.get('r')
        if r is None:
            raise KeyError('REGIONALS 配置必须包含 r key!')
        regional_ids.append(r)
        regional_dict[r] = regional


def getregional(r):
    return regional_dict.get(r)


def init(basedir=None, initr=True):
    """ 初始化配置文件
    :param initr: 是否初始化配置文件中的 initregionals
    """
    global __basedir, cfg_json
    if basedir is None:
        __basedir = os.getcwd()
    else:
        __basedir = basedir
    cfg_json = readjson('config.json')
    if initr:
        init_regionals()

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

from mjp import config

address = 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。
package util

import (
	"encoding/json"
	"fmt"
	"os"
	"path"
	"path/filepath"
	"sync"

	"github.com/gin-gonic/gin"
	"github.com/spf13/viper"
)

// Databases 定义配置中的 DATABASES 字段
type Databases struct {
	URI   string            `mapstructure:"DATABASE_URI"`
	Binds map[string]string `mapstructure:"DATABASE_BINDS"`
}

// Regional 定义一个区服的配置
type Regional struct {
	Name      string `mapstructure:"name"`
	R         int    `mapstructure:"r"`
	BindKeyDb string `mapstructure:"bind_key_db"`
}

// String return a string about regional
func (regional *Regional) String() string {
	return regional.Name
}

/*
Config parse form config.json
*/
type Config struct {
	GinMode   string      `mapstructure:"GIN_MODE"`
	Path      string      `mapstructure:"PATH"`
	Address   string      `mapstructure:"ADDRESS"`
	SecretKey string      `mapstructure:"SECRET_KEY"`
	Databases *Databases  `mapstructure:"DATABASES"`
	Regionals []*Regional `mapstructure:"REGIONALS"`
}

// String return the config as string
func (conf *Config) String() string {
	c := viper.AllSettings()
	bs, err := json.Marshal(c)
	if err != nil {
		fmt.Println(err)
	}
	return string(bs)
}

// Regional return a regional in REGIONALS
func (conf *Config) Regional(r int) *Regional {
	return regionals[r]
}

// DatabaseBind return a database_uri in DATABASES
func (conf *Config) DatabaseBind(bindKey string) string {
	return conf.Databases.Binds[bindKey]
}

var regionals map[int]*Regional
var conf *Config
var configOnce sync.Once

// load the config.json and return it
func load(content string) (*Config, error) {
	config := &Config{}

	var err error

	viper.SetConfigFile(content)
	err = viper.ReadInConfig()
	if err != nil {
		return nil, err
	}

	err = viper.Unmarshal(&config)
	if err != nil {
		return nil, err
	}
	regionals = make(map[int]*Regional)
	for _, value := range config.Regionals {
		regionals[value.R] = value
	}

	return config, nil
}

// ConfigInstance is a signal Config
func ConfigInstance() *Config {
	configOnce.Do(func() {
		configFile := GetDir("config.json")
		inst, err := load(configFile)
		if err != nil {
			panic(err)
		}
		conf = inst
		if conf.GinMode != "" {
			gin.SetMode(conf.GinMode)
		}
	})
	return conf
}

// GetDir return a path in pwd
func GetDir(elem ...string) string {
	curPath, _ := filepath.Abs(filepath.Dir(os.Args[0]))
	args := append([]string{curPath}, elem...)
	return path.Join(args...)
}

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

看看怎么使用 config.go

package middlewares

import (
	"mjp/util"
	"github.com/gin-gonic/gin"
)
var config *util.Config
var logger *util.Logger

func init() {
    config = util.ConfigInstance()
    logger = util.GetAppLogger()
}

func printAddress() {
    logger.Infof("Addrss: %s", config.Address)
}

参考


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

全文完