likes
comments
collection
share

亲手写一个gin server项目-2配置文件

作者站长头像
站长
· 阅读数 12

本系列文章旨在从头到尾写一个admin的gin server项目。

整个项目包括:项目结构,项目启动,配置文件,中间件,日志,格式化响应,表单校验,检索,jwt会话,rbac鉴权,单元测试,文档生成,基于es的单体日志采集,基于prometheus的指标监控......。

所涉及到的技术栈包括:gin、zap、mysql、gorm、redis、es、prometheus 等。

上一篇文章介绍了获取参数及项目context封装。

今天介绍配置文件。

配置文件

http server这种复杂的程序,一般都会有配置文件,数据库账号密码,缓存账号密码,其他参数等等,都会写入配置文件中。

这些虽然可以用命令参数传递到程序,但是没人会真的全部用命令参数进行控制,因为麻烦,而且不能保证每次参数都会输对。

配置文件意义重大,配置文件可以采用json、init、yaml等格式。

json配置文件

json这种配置文件一般在nodjs这样的前端程序里比较常见,属于程序友好型方案,因为所有程序都可以直接解析json,立马使用。

但是却对程序员不友好,格式嵌套且严格,稍不留神就会写错,排查起来没有IDE加持很难看到哪里写错。

这种格式最大的缺陷并不是格式问题,而是没有注释,唯一可以采用的注释方案就是,重复命名。比如:

{"name":"张三"}

你想加注释,那么如下方法:

{"name":"姓名","name":"张三"}

用同一个参数可被覆盖的原理进行注释。

ini配置文件

我曾经就用过一段时间的ini配置文件。

[httpServe]
host = 0.0.0.0:8000

[adminServe]
adminUser = admin
password = 123456

这就是一段ini配置文件,参数可以按照"区"进行划分。mysql就是用的这种配置。

相信聪明的你看出点弊端来。配置文件无法配置数组,也无法配置多层嵌套类型的参数。

yaml & yml

yaml和yml是同一种配置文件,常见的docker-compose等项目都是用的这种格式。

这种配置文件比较灵活。

封装自己的配置文件函数

一个项目。对于配置文件的处理应该包括:配置文件解析,配置参数默认值,配置参数校验。

下边进行少许封装,实现一个yaml文件的解析

配置文件解析

使用库 gopkg.in/yaml.v3 对文件进行解析,这个库是golang中用的最多的yaml解析库。

如果直接使用这个库把文件解析并映射到一个结构体中,那对结构体的key命名有要求,key如果由多个单词组成,默认不支持拓峰写法,只能定义成下划线分割写法。对于yaml文件,key命名首字母必须小写,不然也不能射到结构体,当然如果用golang的tag机制,进行重命名,可以解决上述问题。

如果想解析一个配置文件,其中的key命名和结构体key一模一样,即首字母大写,并且驼峰命名。并且不用tag重写结构体名字。如何解决?

用json进行中转即可实现,用yaml解析成map结构的bytes数据,再用用json还原成struct对象即可。

代码如下:


type YamlConfig struct {
   mapdata map[string]interface{}
   cfg     interface{} //指针类型
}

func NewYamlConfig(cfg interface{}) (*YamlConfig, error) {
   rfv := reflect.ValueOf(cfg)
   if err := utils.ValidatePtr(&rfv); err != nil {
      return nil, err
   }
   return &YamlConfig{cfg: cfg}, nil

}

func (this *YamlConfig) read(in string) error {
   bytes, err := ioutil.ReadFile(in)
   if err != nil {
      return err
   }

   if err := yaml.Unmarshal(bytes, &this.mapdata); err != nil {
      return err
   }
   return nil
}


func (this *YamlConfig) jsonDecode() error {
   bytes, err := json.Marshal(this.mapdata)
   if err != nil {
      return err
   }
   // fmt.Println(string(bt), err)
   if err := json.Unmarshal(bytes, this.cfg); err != nil {
      return err
   }
   return nil
}

我一般都会把功能组织到同一个结构体中,尽量收敛功能。

上边代码中 read 函数中即把读取到的配置文件保存在 mapdata中。jsonDecode则把map数据反序列化到struct中。

默认值设置

默认值用库github.com/creasty/defaults,struct只需要用tag即可配置默认值。 比如下边这个结构体:

type Mysql struct{
    Host int `default:"127.0.0.1:3306"`
}

把这个库封装进来:

func (this *YamlConfig) doDefault() error {
   return defaults.Set(this.cfg)
}

参数校验

校验使用库 gopkg.in/dealancer/validate.v2。校验规则请上GitHub看说明。

这个库经常用的规则是"非空",比如下边代码:

type Mysql struct {
   User     string `validate:"empty=false"`
   Password string `validate:"empty=false"`
   Host     string `default:"localhost:3306"`
   DbName   string `validate:"empty=false"`
}

封装进代码里:

func (this *YamlConfig) doValide() error {
   return validate.Validate(this.cfg)
}

实用代码

最后在把所有流程穿起来,封装成一个简单实用的函数,给一个配置文件路径,就能把一个struct对象参数化。

完整代码如下:

func Load(configFile string, in interface{}) error {
   cfg, err := NewYamlConfig(in)
   if err != nil {
      return err
   }

   if err := cfg.read(configFile); err != nil {
      return err
   }

   if err := cfg.jsonDecode(); err != nil {
      return err
   }

   if err := cfg.doDefault(); err != nil {
      return err
   }

   if err := cfg.doValide(); err != nil {
      return err
   }
   return nil
}

func MustLoad(configFile string, in interface{}) {
   if err := Load(configFile, in); err != nil {
      panic(err)
   }
}

最后只用MustLoad 即可完成配置文件加载。

项目添加配置文件代码

昨天的代码稍微改动一下。

把昨天的命令行参数从端口参数变成配置文件参数,端口参数写在配置文件中。

代码如下:

var config = flag.String("f", "config.yaml", "配置文件")

type Config struct {
   Host string `default:":9000"`
}

func main() {

   flag.Usage = Usage
   flag.Parse()
   
   //配置文件
   var C Config
   if err := yamlconfig.Load(*config, &C); err != nil {
      println(err)
      os.Exit(1)
   }

   if len(os.Args) < 2 {
      flag.Usage()
      os.Exit(1)
   }
   cmder := os.Args[1]
   switch cmder {
   case "run":
      server(C)
   default:
      flag.Usage()
      os.Exit(1)
   }
}

type ServerContext struct {
   Src    string
   Config Config //配置文件
}

func NewServerContext(c Config) *ServerContext {
   return &ServerContext{
      Config: c,
      Src:    "这是测试资源",
   }
}

func server(c Config) {
   r := gin.Default()
   svc := NewServerContext(c)

   r.GET("/", Handler(svc))
   r.Run(C.Host)
}

func Handler(svc *ServerContext) gin.HandlerFunc {
   return func(c *gin.Context) {
      c.JSON(200, gin.H{
         "Example": "Hello Gin",
         "src":     svc.Src,
         "Host":    svc.Config.Host,
      })
   }
}

func Usage() {
   fmt.Println("http server v1.0")
   fmt.Println("main run -f config.yaml")
   fmt.Println()
   fmt.Println("参数:")
   flag.PrintDefaults()
}

上述代码初始化了配置对象,并且把配置文件注入到context中。

结语

这次写这些内容花费了不少时间,也反反复复修改了许多,为了保证代码的准确性,对yaml库进行了重新测试,一直以为这个库无法解析yaml关键字$#,但是测试发现完全没有问题。

这次也写了好几段关于viper的内容,但是最后想了想,虽然这东西好,但是项目没有,写多了反而不太好,就又删了。

技术写作的意义就在此吧,每次写作都是对知识的重新梳理。

目前所有代码都是写在同一个文件里,测试比较方便。后边内容丰满的时候,会进行一个项目结构重构的。

明天会写一些日志的内容。

不单单是http server项目,大部分项目基本上都有参数获取、配置文件、日志,三大基础功能。