zoukankan      html  css  js  c++  java
  • Go 每日一库之 go-ini

    简介

    ini 是 Windows 上常用的配置文件格式。MySQL 的 Windows 版就是使用 ini 格式存储配置的。
    go-ini是 Go 语言中用于操作 ini 文件的第三方库。

    本文介绍go-ini库的使用。

    快速使用

    go-ini 是第三方库,使用前需要安装:

    $ go get gopkg.in/ini.v1
    

    也可以使用 GitHub 上的仓库:

    $ go get github.com/go-ini/ini
    

    首先,创建一个my.ini配置文件:

    app_name = awesome web
    
    # possible values: DEBUG, INFO, WARNING, ERROR, FATAL
    log_level = DEBUG
    
    [mysql]
    ip = 127.0.0.1
    port = 3306
    user = dj
    password = 123456
    database = awesome
    
    [redis]
    ip = 127.0.0.1
    port = 6381
    

    使用 go-ini 库读取:

    package main
    
    import (
      "fmt"
      "log"
    
      "gopkg.in/ini.v1"
    )
    
    func main() {
      cfg, err := ini.Load("my.ini")
      if err != nil {
        log.Fatal("Fail to read file: ", err)
      }
    
      fmt.Println("App Name:", cfg.Section("").Key("app_name").String())
      fmt.Println("Log Level:", cfg.Section("").Key("log_level").String())
    
      fmt.Println("MySQL IP:", cfg.Section("mysql").Key("ip").String())
      mysqlPort, err := cfg.Section("mysql").Key("port").Int()
      if err != nil {
        log.Fatal(err)
      }
      fmt.Println("MySQL Port:", mysqlPort)
      fmt.Println("MySQL User:", cfg.Section("mysql").Key("user").String())
      fmt.Println("MySQL Password:", cfg.Section("mysql").Key("password").String())
      fmt.Println("MySQL Database:", cfg.Section("mysql").Key("database").String())
    
      fmt.Println("Redis IP:", cfg.Section("redis").Key("ip").String())
      redisPort, err := cfg.Section("redis").Key("port").Int()
      if err != nil {
        log.Fatal(err)
      }
      fmt.Println("Redis Port:", redisPort)
    }
    

    在 ini 文件中,每个键值对占用一行,中间使用=隔开。以#开头的内容为注释。ini 文件是以分区(section)组织的。
    分区以[name]开始,在下一个分区前结束。所有分区前的内容属于默认分区,如my.ini文件中的app_namelog_level

    使用go-ini读取配置文件的步骤如下:

    • 首先调用ini.Load加载文件,得到配置对象cfg
    • 然后以分区名调用配置对象的Section方法得到对应的分区对象section,默认分区的名字为"",也可以使用ini.DefaultSection
    • 以键名调用分区对象的Key方法得到对应的配置项key对象;
    • 由于文件中读取出来的都是字符串,key对象需根据类型调用对应的方法返回具体类型的值使用,如上面的StringMustInt方法。

    运行以下程序,得到输出:

    App Name: awesome web
    Log Level: DEBUG
    MySQL IP: 127.0.0.1
    MySQL Port: 3306
    MySQL User: dj
    MySQL Password: 123456
    MySQL Database: awesome
    Redis IP: 127.0.0.1
    Redis Port: 6381
    

    配置文件中存储的都是字符串,所以类型为字符串的配置项不会出现类型转换失败的,故String()方法只返回一个值。
    但如果类型为Int/Uint/Float64这些时,转换可能失败。所以Int()/Uint()/Float64()返回一个值和一个错误。

    要留意这种不一致!如果我们将配置中 redis 端口改成非法的数字 x6381,那么运行程序将报错:

    2020/01/14 22:43:13 strconv.ParseInt: parsing "x6381": invalid syntax
    

    Must*便捷方法

    如果每次取值都需要进行错误判断,那么代码写起来会非常繁琐。为此,go-ini也提供对应的MustType(Type 为Init/Uint/Float64等)方法,这个方法只返回一个值。
    同时它接受可变参数,如果类型无法转换,取参数中第一个值返回,并且该参数设置为这个配置的值,下次调用返回这个值:

    package main
    
    import (
      "fmt"
      "log"
    
      "gopkg.in/ini.v1"
    )
    
    func main() {
      cfg, err := ini.Load("my.ini")
      if err != nil {
        log.Fatal("Fail to read file: ", err)
      }
    
      redisPort, err := cfg.Section("redis").Key("port").Int()
      if err != nil {
        fmt.Println("before must, get redis port error:", err)
      } else {
        fmt.Println("before must, get redis port:", redisPort)
      }
    
      fmt.Println("redis Port:", cfg.Section("redis").Key("port").MustInt(6381))
    
      redisPort, err = cfg.Section("redis").Key("port").Int()
      if err != nil {
        fmt.Println("after must, get redis port error:", err)
      } else {
        fmt.Println("after must, get redis port:", redisPort)
      }
    }
    

    配置文件还是 redis 端口为非数字 x6381 时的状态,运行程序:

    before must, get redis port error: strconv.ParseInt: parsing "x6381": invalid syntax
    redis Port: 6381
    after must, get redis port: 6381
    

    我们看到第一次调用Int返回错误,以 6381 为参数调用MustInt之后,再次调用Int,成功返回 6381。MustInt源码也比较简单:

    // gopkg.in/ini.v1/key.go
    func (k *Key) MustInt(defaultVal ...int) int {
      val, err := k.Int()
      if len(defaultVal) > 0 && err != nil {
        k.value = strconv.FormatInt(int64(defaultVal[0]), 10)
        return defaultVal[0]
      }
      return val
    }
    

    分区操作

    获取信息

    在加载配置之后,可以通过Sections方法获取所有分区,SectionStrings()方法获取所有分区名。

    sections := cfg.Sections()
    names := cfg.SectionStrings()
    
    fmt.Println("sections: ", sections)
    fmt.Println("names: ", names)
    

    运行输出 3 个分区:

    [DEFAULT mysql redis]
    

    调用Section(name)获取名为name的分区,如果该分区不存在,则自动创建一个分区返回:

    newSection := cfg.Section("new")
    
    fmt.Println("new section: ", newSection)
    fmt.Println("names: ", cfg.SectionStrings())
    

    创建之后调用SectionStrings方法,新分区也会返回:

    names:  [DEFAULT mysql redis new]
    

    也可以手动创建一个新分区,如果分区已存在,则返回错误:

    err := cfg.NewSection("new")
    

    父子分区

    在配置文件中,可以使用占位符%(name)s表示用之前已定义的键name的值来替换,这里的s表示值为字符串类型:

    NAME = ini
    VERSION = v1
    IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s
    
    [package]
    CLONE_URL = https://%(IMPORT_PATH)s
    
    [package.sub]
    

    上面在默认分区中设置IMPORT_PATH的值时,使用了前面定义的NAMEVERSION
    package分区中设置CLONE_URL的值时,使用了默认分区中定义的IMPORT_PATH

    我们还可以在分区名中使用.表示两个或多个分区之间的父子关系,例如package.sub的父分区为packagepackage的父分区为默认分区。
    如果某个键在子分区中不存在,则会在它的父分区中再次查找,直到没有父分区为止:

    cfg, err := ini.Load("parent_child.ini")
    if err != nil {
      fmt.Println("Fail to read file: ", err)
      return
    }
    
    fmt.Println("Clone url from package.sub:", cfg.Section("package.sub").Key("CLONE_URL").String())
    

    运行程序输出:

    Clone url from package.sub: https://gopkg.in/ini.v1
    

    子分区中package.sub中没有键CLONE_URL,返回了父分区package中的值。

    保存配置

    有时候,我们需要将生成的配置写到文件中。例如在写工具的时候。保存有两种类型的接口,一种直接保存到文件,另一种写入到io.Writer中:

    err = cfg.SaveTo("my.ini")
    err = cfg.SaveToIndent("my.ini", "	")
    
    cfg.WriteTo(writer)
    cfg.WriteToIndent(writer, "	")
    

    下面我们通过程序生成前面使用的配置文件my.ini并保存:

    package main
    
    import (
      "fmt"
      "os"
    
      "gopkg.in/ini.v1"
    )
    
    func main() {
      cfg := ini.Empty()
    
      defaultSection := cfg.Section("")
      defaultSection.NewKey("app_name", "awesome web")
      defaultSection.NewKey("log_level", "DEBUG")
    
      mysqlSection, err := cfg.NewSection("mysql")
      if err != nil {
        fmt.Println("new mysql section failed:", err)
        return
      }
      mysqlSection.NewKey("ip", "127.0.0.1")
      mysqlSection.NewKey("port", "3306")
      mysqlSection.NewKey("user", "root")
      mysqlSection.NewKey("password", "123456")
      mysqlSection.NewKey("database", "awesome")
    
      redisSection, err := cfg.NewSection("redis")
      if err != nil {
        fmt.Println("new redis section failed:", err)
        return
      }
      redisSection.NewKey("ip", "127.0.0.1")
      redisSection.NewKey("port", "6381")
    
      err = cfg.SaveTo("my.ini")
      if err != nil {
        fmt.Println("SaveTo failed: ", err)
      }
    
      err = cfg.SaveToIndent("my-pretty.ini", "	")
      if err != nil {
        fmt.Println("SaveToIndent failed: ", err)
      }
    
      cfg.WriteTo(os.Stdout)
      fmt.Println()
      cfg.WriteToIndent(os.Stdout, "	")
    }
    

    运行程序,生成两个文件my.inimy-pretty.ini,同时控制台输出文件内容。

    my.ini

    app_name  = awesome web
    log_level = DEBUG
    
    [mysql]
    ip       = 127.0.0.1
    port     = 3306
    user     = root
    password = 123456
    database = awesome
    
    [redis]
    ip   = 127.0.0.1
    port = 6381
    

    my-pretty.ini

    app_name  = awesome web
    log_level = DEBUG
    
    [mysql]
    	ip       = 127.0.0.1
    	port     = 3306
    	user     = root
    	password = 123456
    	database = awesome
    
    [redis]
    	ip   = 127.0.0.1
    	port = 6381
    

    *Indent方法会对子分区下的键增加缩进,看起来美观一点。

    分区与结构体字段映射

    定义结构变量,加载完配置文件后,调用MapTo将配置项赋值到结构变量的对应字段中。

    package main
    
    import (
      "fmt"
    
      "gopkg.in/ini.v1"
    )
    
    type Config struct {
      AppName   string `ini:"app_name"`
      LogLevel  string `ini:"log_level"`
    
      MySQL     MySQLConfig `ini:"mysql"`
      Redis     RedisConfig `ini:"redis"`
    }
    
    type MySQLConfig struct {
      IP        string `ini:"ip"`
      Port      int `ini:"port"`
      User      string `ini:"user"`
      Password  string `ini:"password"`
      Database  string `ini:"database"`
    }
    
    type RedisConfig struct {
      IP      string `ini:"ip"`
      Port    int `ini:"port"`
    }
    
    func main() {
      cfg, err := ini.Load("my.ini")
      if err != nil {
        fmt.Println("load my.ini failed: ", err)
      }
    
      c := Config{}
      cfg.MapTo(&c)
    
      fmt.Println(c)
    }
    

    MapTo内部使用了反射,所以结构体字段必须都是导出的。如果键名与字段名不相同,那么需要在结构标签中指定对应的键名。
    这一点与 Go 标准库encoding/jsonencoding/xml不同。标准库json/xml解析时可以将键名app_name对应到字段名AppName
    或许这是go-ini库可以优化的点?

    先加载,再映射有点繁琐,直接使用ini.MapTo将两步合并:

    err = ini.MapTo(&c, "my.ini")
    

    也可以只映射一个分区:

    mysqlCfg := MySQLConfig{}
    err = cfg.Section("mysql").MapTo(&mysqlCfg)
    

    还可以通过结构体生成配置:

    cfg := ini.Empty()
    
    c := Config {
      AppName: 	"awesome web",
      LogLevel: 	"DEBUG",
      MySQL: MySQLConfig {
        IP: 	"127.0.0.1",
        Port:	3306,
        User:	"root",
        Password:"123456",
        Database:"awesome",
      },
      Redis: RedisConfig {
        IP:		"127.0.0.1",
        Port:	6381,
      },
    }
    
    err := ini.ReflectFrom(cfg, &c)
    if err != nil {
      fmt.Println("ReflectFrom failed: ", err)
      return
    }
    
    err = cfg.SaveTo("my-copy.ini")
    if err != nil {
      fmt.Println("SaveTo failed: ", err)
      return
    }
    

    总结

    本文介绍了go-ini库的基本用法和一些有趣的特性。示例代码已上传GitHub
    其实go-ini还有很多高级特性。官方文档非常详细,推荐去看,而且有中文哟~
    作者无闻,相信做 Go 开发的都不陌生。

    参考

    1. go-ini GitHub 仓库
    2. go-ini 官方文档

    我的博客

    欢迎关注我的微信公众号【GoUpUp】,共同学习,一起进步~

    本文由博客一文多发平台 OpenWrite 发布!

  • 相关阅读:
    mysql修改密码策略
    YUM方法安装mysql5.7版本
    redis-5.0.5安装(linux centos)
    centos7 安装php7扩展
    Linux df
    Spotlight监控工具使用
    Linux 安装iostat命令
    转载:数据库连接池到底应该设置多大?
    cqlsh 一个错误
    Linux Top命令详解
  • 原文地址:https://www.cnblogs.com/darjun/p/12199477.html
Copyright © 2011-2022 走看看