不管是移动应用、桌面程序还是后台服务,经常需要从配置文件中读取配置信息,进行程序初始化和改变运行时的状态。以什么要的格式来存储配置信息,这是开发人员需要面临的一个问题。常用的配置文件格式主要有:
下面会详细介绍并给出解析实例。
键值对是一个非常简单易用的配置文件格式。每一个键值对表示一项配置,键值对的分隔符一般使用等号或冒号。解析时,可以将 # 号开始的行视为注释行,以达到注释的功能。以键值对为表现形式的配置文件格式常见的有 Windows .ini 文件和 Java 中的 .properties 文件。
例如下面是一个使用键值对表示的后台服务配置。
# This is a comment
name=UserProfileServer
maxconns=1000
queuecap=10000
queuetimeout=300
loglevel=ERROR
logsize=10M
lognum=10
logpath=/usr/local/app/log
在解析上面的配置时,可以按行读取,然后放到 map 中。下面以 Go 为例,完成对上面配置文件的解析。
package main
import(
"bufio"
"io"
"os"
"fmt"
"errors"
"strings"
)
func ParseConf(confPath string) (map[string]string, error) {
if confPath == ""{
return nil, errors.New("param is ill")
}
f, err := os.Open(confPath)
if err != nil {
return nil, err
}
defer f.Close()
//store config info
m := make(map[string]string)
bfRd := bufio.NewReader(f)
//read by line, the line terminator is '\n'
for{
line, err := bfRd.ReadString('\n')
if err == nil || err == io.EOF{
//ignore blank and comment line
if strings.TrimSpace(line) != "" && strings.TrimSpace(line)[0] != '#'{
vKV := strings.Split(line, "=")
if len(vKV) == 2 {
m[vKV[0]] = vKV[1]
}
}
if err == io.EOF{
return m, nil
}
} else {
return nil, err
}
}
return m, nil
}
func main(){
mConf, _ := ParseConf("server.kv")
fmt.Println(mConf)
}
运行结果:
map[loglevel:ERROR
lognum:10
logpath:/usr/local/app/log
logsize:10M
maxconns:1000
name:UserProfileServer
queuecap:10000
queuetimeout:300
]
JSON(JavaScript Object Notation) 是轻量级的文本数据交换格式,独立于语言,具有自我描述性。JSON 类似于 XML,但是 JSON 比 XML 更小、更快,更易解析。
JSON 语法是 JavaScript 对象表示法语法的子集。
名称/值对包括字段名称(在双引号中),后面写一个冒号,然后是值:
"firstName" : "John"
JSON 值可以是:
JSON 对象在花括号中书写,对象可以包含多个名称/值对,使用逗号分隔:
{ "firstName":"John" , "lastName":"Doe" }
JSON 数组在方括号中书写,数组可包含多个对象:
{
"employees": [
{ "firstName":"John" , "lastName":"Doe" },
{ "firstName":"Anna" , "lastName":"Smith" },
{ "firstName":"Peter" , "lastName":"Jones" }
]
}
下面以 XML 表示一个简单的后台服务配置:
{
"-name": "UserProfileServer",
"maxconns": "1000",
"queuecap": "10000",
"queuetimeout": "300",
"loginfo": {
"loglevel": "ERROR",
"logsize": "10M",
"lognum": "10",
"logpath": "/usr/local/app/log"
}
}
其中 -name 表示服务的名称,前面一个横杠表示该值可以转换为 XML 的标签属性。其它的名称/值对表示各个服务的配置项。
下面以 Go 为例,利用 Go 自带的 JSON 包 encoding/json 完成对上面服务配置的解析。
第一步,将 JSON 串转换为 Go struct。把上面的 JSON 串粘贴到 Convert JSON to Go struct。
type Server struct {
Name string `json:"-name"`
Maxconns string `json:"maxconns"`
Queuecap string `json:"queuecap"`
Queuetimeout string `json:"queuetimeout"`
Loginfo struct {
Loglevel string `json:"loglevel"`
Logsize string `json:"logsize"`
Lognum string `json:"lognum"`
Logpath string `json:"logpath"`
} `json:"loginfo"`
}
第二步,利用 Go 自带的 JSON 包 encoding/json 解析上面以 JSON 串表示的配置信息。
package main
import(
"encoding/json"
"io/ioutil"
"fmt"
"os"
)
func main() {
file, err := os.Open("server.json")
if err != nil {
fmt.Printf("error:%v\n", err)
return
}
defer file.Close()
data, err := ioutil.ReadAll(file)
if err != nil {
fmt.Printf("error: %v", err)
return
}
v := Server{}
err = json.Unmarshal(data, &v)
if err != nil {
fmt.Printf("error:%v\n", err)
return
}
fmt.Printf("%+v\n", v)
}
运行输出:
{Name:UserProfileServer Maxconns:1000 Queuecap:10000 Queuetimeout:300 Loginfo:{Loglevel:ERROR Logsize:10M Lognum:10 Logpath:/usr/local/app/log}}
XML(Extensible Markup Language)是可扩展标记语言,用来传输和存储数据。因为其允许用户自定义标记名称,具有自我描述性,可灵活地用于存储服务配置信息。
XML 文档结构是一种树结构,它从“根部”开始,然后扩展到“枝叶”。XML 文档必须有一个唯一的根结点,根结点包含所有其它结点。所有结点均可拥有文本内容和属性(名称/值的对)。XML 结点也叫做 XML 元素。
编写 XML 文档时,还需要注意以下几点: (1)所有 XML 元素都须有关闭标签; (2)XML 标签对大小写敏感; (3)XML 的属性值须加引号; (4)XML 中的特殊字符可以使用实体引用来表示。在 XML 中,有 5 个预定义的实体引用:
实体引用 | 字符 | 名称 |
---|---|---|
< | < | 小于 |
> | > | 大于 |
& | & | 和号 |
' | ’ | 单引号 |
" | " | 引号 |
(5)在 XML 中编写注释的语法与 HTML 的语法很相似:
<!-- This is a comment -->
(6)XML 元素必须遵循以下命名规则:
下面以 XML 表示一个简单的后台服务配置:
<?xml version="1.0" encoding="UTF-8"?>
<server name="UserProfileServer">
<maxconns>1000</maxconns>
<queuecap>10000</queuecap>
<queuetimeout>300</queuetimeout>
<loginfo>
<loglevel>ERROR</loglevel>
<logsize>10M</logsize>
<lognum>10</lognum>
<logpath>/usr/local/app/log</logpath>
</loginfo>
</server>
第一行是 XML 声明,它定义 XML 的版本(1.0)和所使用的编码(UTF-8)。紧接着 server 为根结点,name 为根结点的一个属性,表示服务名称,其他子结点的文本内容表示服务的具体配置项。
使用 XML 存储服务配置信息,我们如何解析呢?下面以 Go 为例,来解析上面的 XML 格式的服务配置。
第一步,将上面的 XML 配置信息粘贴到 XML to Go struct 快速获取 Go struct 的定义。
type Server struct {
Name string `xml:"name,attr"` //标签属性
Maxconns string `xml:"maxconns"`
Queuecap string `xml:"queuecap"`
Queuetimeout string `xml:"queuetimeout"`
Loginfo struct {
Loglevel string `xml:"loglevel"`
Logsize string `xml:"logsize"`
Lognum string `xml:"lognum"`
Logpath string `xml:"logpath"`
} `xml:"loginfo"`
}
第二步,借助 Go 自带的 encoding/xml 包完成 XML 文档解析。
package main
import(
"encoding/xml"
"io/ioutil"
"fmt"
"os"
)
func main() {
file, err := os.Open("server.xml")
if err != nil {
fmt.Printf("error:%v\n", err)
return
}
defer file.Close()
data, err := ioutil.ReadAll(file)
if err != nil {
fmt.Printf("error: %v", err)
return
}
v := Server{}
err = xml.Unmarshal(data, &v)
if err != nil {
fmt.Printf("error:%v\n", err)
return
}
fmt.Printf("%+v\n", v)
}
运行输出:
{Name:UserProfileServer Maxconns:1000 Queuecap:10000 Queuetimeout:300 Loginfo:{Loglevel:ERROR Logsize:10M Lognum:10 Logpath:/usr/local/app/log}}
YAML(YAML Ain’t a Markup Language)是专门用来写配置文件的语言,非常简洁和强大,相比于 JSON 和 XML ,更加便于开发人员读写。
YAML 的基本语法规则如下:
#
表示注释,从这个字符一直到行尾,都会被解析器忽略。
YAML 支持的数据结构有三种:
下面分别介绍这三种数据结构。
对象的一组键值对,使用冒号结构表示,注意冒号后面要加一个空格。
animal: pets
YAML 也允许另一种写法,将所有键值对写成一个行内对象。
hash: { name: Steve, foo: bar }
一组以连字符开头的行,构成一个数组。
- Cat
- Dog
- Goldfish
如果数据结构的子成员是一个数组,则可以在该项下面缩进一个空格。
-
- Cat
- Dog
- Goldfish
数组也可以采用行内表示法。
animal: [Cat,Dog]
注意,元素间不需要加空格。
对象和数组可以结合使用,形成复合结构。
languages:
- Ruby
- Perl
- Python
websites:
YAML: yaml.org
Ruby: ruby-lang.org
Python: python.org
Perl: use.perl.org
对应的 JSON 表示如下:
{
"languages": [
"Ruby",
"Perl",
"Python"
],
"websites": {
"YAML": "yaml.org",
"Ruby": "ruby-lang.org",
"Python": "python.org",
"Perl": "use.perl.org"
}
}
纯量是最基本的,不可再分的值,包括:
使用一个例子来快速了解纯量的基本使用:
boolean:
- TRUE #true、True 都可以
- FALSE #false、False 都可以
float:
- 3.14 #数值直接以字面量的形式表示
- 6.8523015e+5 #可以使用科学计数法
int:
- 123 #数值直接以字面量的形式表示
- 0b1010_0111_0100_1010_1110 #二进制表示
null:
nodeName: 'node'
parent: ~ #使用~表示null
string:
- 哈哈 #字符串默认不使用引号
- 'Hello world' #可以使用双引号或者单引号包裹特殊字符
- newline
newline2 #字符串可以拆成多行,每一换行符会被转化成一个空格
date:
- 2018-02-17 #日期必须使用ISO 8601格式,即 yyyy-MM-dd
datetime:
- 2018-02-17T15:02:31+08:00 #时间使用 ISO 8601 格式,时间和日期之间使用 T 连接,最后使用 + 代表时区
锚点 & 和别名 *,可以用来引用。
defaults: &defaults
adapter: postgres
host: localhost
development:
database: myapp_development
<<: *defaults
等同于下面的配置。
defaults:
adapter: postgres
host: localhost
development:
database: myapp_development
adapter: postgres
host: localhost
& 用来建立锚点(defaults),<< 表示合并到当前数据,* 用来引用锚点。
下面以 YAML 表示一个简单的后台服务配置:
name: UserProfileServer
maxconns: 1000
queuecap: 10000
queuetimeout: 300
loginfo:
loglevel: ERROR
logsize: 10M
lognum: 10
logpath: /usr/local/app/log
因为 Go 并没有提供解析 YAML 的标准库,所以这里基于第三方开源库 go-yaml 来完成对 YAML 文件的解析。
第一步,将 YAML 配置文件的内容在 Convert YAML to Go struct 转换为 Go struct。
type Server struct {
Name string `yaml:"name"`
Maxconns int `yaml:"maxconns"`
Queuecap int `yaml:"queuecap"`
Queuetimeout int `yaml:"queuetimeout"`
Loginfo struct {
Loglevel string `yaml:"loglevel"`
Logsize string `yaml:"logsize"`
Lognum int `yaml:"lognum"`
Logpath string `yaml:"logpath"`
} `yaml:"loginfo"`
}
第二步,利用第三方开源库 go-yaml 来完成对 YAML 文件的解析。
package main
import(
"io/ioutil"
"fmt"
"os"
"yaml"
)
func main() {
file, err := os.Open("server.yaml")
if err != nil {
fmt.Printf("error:%v\n", err)
return
}
defer file.Close()
data, err := ioutil.ReadAll(file)
if err != nil {
fmt.Printf("error:%v\n", err)
return
}
v := Server{}
err = yaml.Unmarshal(data, &v)
if err != nil {
fmt.Printf("error:%v\n", err)
return
}
fmt.Printf("%+v\n", v)
}
运行输出:
{Name:UserProfileServer Maxconns:1000 Queuecap:10000 Queuetimeout:300 Loginfo:{Loglevel:ERROR Logsize:10M Lognum:10 Logpath:/usr/local/app/log}}
GitHub 联合创始人 Tom Preston-Werner 觉得 YAML 不够简洁优雅,因此和其他几位开发者一起捣鼓出了一个 TOML(Tom’s Obvious Minimal Language)。TOML 旨在成为一个语义显著且易于阅读的极简配置文件格式,能够无歧义地转化为哈希表,且能够简单地解析成形形色色语言中的数据结构,用于取代 YAML 和 JSON。
TOML 的基本语法规则如下:
TOML 文档最基本的构成区块是键/值对。
key = "value"
值必须是这些类型:字符串,整数,浮点数,布尔值,日期时刻,数组或行内表。不指定值是有误的。
键名可以是裸露的,引号引起来的,或点分隔的。
裸键只能包含 ASCII 字母,ASCII 数字,下划线和短横线(A-Za-z0-9_-)。
key = "value"
bare_key = "value"
bare-key = "value"
1234 = "value"
引号键遵循与基础字符串或字面量字符串相同的规则并允许你使用更为广泛的键名。除非明显必要,使用裸键方为最佳实践。
"127.0.0.1" = "value"
"character encoding" = "value"
"ʎǝʞ" = "value"
'key2' = "value"
'quoted "value"' = "value"
点分隔键是一系列通过点相连的裸键或引号键。这允许了你将相近属性放在一起:
"名称" = "橙子"
"物理属性"."颜色" = "橙色"
"物理属性"."形状" = "圆形"
site."google.com" = true
这在 JSON 那儿,是以下结构:
{
"名称": "橙子",
"物理属性": {
"颜色": "橙色",
"形状": "圆形"
},
"site": {
"google.com": true
}
}
点分隔符周围的空白会被忽略,不过,最佳实践是不要使用任何不必要的空白。
多次定义同一个键是不行的。
# 不要这样做
name = "Tom"
name = "Pradyun"
共有四种方式来表示字符串:基础式,多行基础式,字面量式,和多行字面量式。所有字符串都只能包含有效的 UTF-8 字符。
任何 Unicode 字符都可以使用,除了那些必须转义的:引号,反斜杠,以及控制字符(U+0000 至 U+001F,U+007F)。
str = "我是一个字符串。\"你可以把我引起来\"。姓名\tJos\u00E9\n位置\t旧金山。"
为了方便,一些流行的字符有其简便转义写法。
\b - backspace (U+0008)
\t - tab (U+0009)
\n - linefeed (U+000A)
\f - form feed (U+000C)
\r - carriage return (U+000D)
\" - quote (U+0022)
\\ - backslash (U+005C)
\uXXXX - unicode (U+XXXX)
\UXXXXXXXX - unicode (U+XXXXXXXX)
任何 Unicode 字符都可以用 \uXXXX 或 \UXXXXXXXX 的形式来转义。转义码必须是有效的 Unicode 标量值。
所有上面未列出的其它转义序列都是保留的,如果被用了,TOML 应当生成一个错误。
有时你需要表示一小篇文本(例如译文)或者想要对非常长的字符串进行折行。TOML 对此进行了简化。
多行基础字符串由三个引号包裹,允许折行。紧随开头引号的那个换行会被去除。其它空白和换行符会被原样保留。
str1 = """
玫瑰是红色的
紫罗兰是蓝色的"""
TOML 解析器可以相对灵活地解析成对所在平台有效的换行字符。
# 在 Unix 系统,上面的多行字符串可能等同于:
str2 = "玫瑰是红色的\n紫罗兰是蓝色的"
# 在 Windows 系统,它可能等价于:
str3 = "玫瑰是红色的\r\n紫罗兰是蓝色的"
想书写长字符串却不想引入无关空白,可以用“行末反斜杠”。当一行的最后一个非空白字符是 \ 时,它会连同它后面的所有空白(包括换行)一起被去除,直到下一个非空白字符或结束引号为止。所有对基础字符串有效的转义序列,对多行基础字符串也同样适用。
# 下列字符串的每一个字节都完全相同:
str1 = "那只 敏捷的 棕 狐狸 跳 过了 那只 懒 狗。"
str2 = """
那只 敏捷的 棕 \
狐狸 跳 过了 \
那只 懒 狗。"""
str3 = """\
那只 敏捷的 棕 \
狐狸 跳 过了 \
那只 懒 狗。\
"""
任何 Unicode 字符都可以使用,除了那些必须被转义的:反斜杠和控制字符(U+0000 至 U+001F,U+007F)。引号不需要转义,除非它们的存在会造成一个比预期提前的结束标记。
如果你常常要指定 Windows 路径或正则表达式,那么必须转义反斜杠就马上成为啰嗦而易错的了。为了帮助搞定这点,TOML 支持字面量字符串,它完全不允许转义。
字面量字符串由单引号包裹。类似于基础字符串,他们只能表现为单行:
# 所见即所得。
winpath = 'C:\Users\nodejs\templates'
winpath2 = '\\ServerX\admin$\system32\'
quoted = '汤姆·"达布斯"·普雷斯顿—维尔纳'
regex = '<\i\c*\s*>'
由于没有转义,无法在由单引号包裹的字面量字符串中写入单引号。万幸,TOML 支持一种多行版本的字面量字符串来解决这个问题。
多行字面量字符串两侧各有三个单引号来包裹,允许换行。类似于字面量字符串,无论任何转义都不存在。 紧随开始标记的那个换行会被剔除。 开始结束标记之间的所有其它内容会原样对待。
regex2 = '''I [dw]on't need \d{2} apples'''
lines = '''
原始字符串中的
第一个换行被剔除了。
所有其它空白
都保留了。
'''
除 tab 以外的所有控制字符都不允许出现在字面量字符串中。因此,对于二进制数据,建议你使用 Base64 或其它合适的 ASCII 或 UTF-8 编码。对那些编码的处理方式,将交由应用程序自己来确定。
整数是纯数字。正数可以有加号前缀。负数的前缀是减号。
int1 = +99
int2 = 42
int3 = 0
int4 = -17
对于大数,你可以在数字之间用下划线来增强可读性。每个下划线两侧必须至少有一个数字。
int5 = 1_000
int6 = 5_349_221
int7 = 1_2_3_4_5 # 无误但不鼓励
前导零是不允许的。整数值 -0 与 +0 是有效的,并等同于无前缀的零。
非负整数值也可以用十六进制、八进制或二进制来表示。在这些格式中,+ 不被允许,而(前缀后的)前导零是允许的。十六进制值大小写不敏感。数字间的下划线是允许的(但不能存在于前缀和值之间)。
# 带有 `0x` 前缀的十六进制
hex1 = 0xDEADBEEF
hex2 = 0xdeadbeef
hex3 = 0xdead_beef
# 带有 `0o` 前缀的八进制
oct1 = 0o01234567
oct2 = 0o755 # 对于表示 Unix 文件权限很有用
# 带有 `0b` 前缀的二进制
bin1 = 0b11010110
取值范围要求为 64 比特(signed long)(−9,223,372,036,854,775,808 至 9,223,372,036,854,775,807)。
浮点数应当被实现为 IEEE 754 binary64 值。
一个浮点数由一个整数部分(遵从与十进制整数值相同的规则)后跟上一个小数部分和/或一个指数部分组成。 如果小数部分和指数部分兼有,那小数部分必须在指数部分前面。
# 小数
flt1 = +1.0
flt2 = 3.1415
flt3 = -0.01
# 指数
flt4 = 5e+22
flt5 = 1e6
flt6 = -2E-2
# 都有
flt7 = 6.626e-34
小数部分是一个小数点后跟一个或多个数字。 一个指数部分是一个 E(大小写均可)后跟一个整数部分(遵从与十进制整数值相同的规则)。 与整数相似,你可以使用下划线来增强可读性。每个下划线必须被至少一个数字围绕。
flt8 = 224_617.445_991_228
浮点数值 -0.0 与 +0.0 是有效的,并且应当遵从 IEEE 754。
特殊浮点值也能够表示。 它们是小写的。
# 无穷
sf1 = inf # 正无穷
sf2 = +inf # 正无穷
sf3 = -inf # 负无穷
# 非数
sf4 = nan # 实际上对应信号非数码还是静默非数码,取决于实现
sf5 = +nan # 等同于 `nan`
sf6 = -nan # 有效,实际码取决于实现
布尔值就是你所惯用的那样。要小写。
bool1 = true
bool2 = false
要明确无误地表示世上的一个特定时间,你可以使用指定了时区偏移量的 RFC 3339 格式的日期时刻。
odt1 = 1979-05-27T07:32:00Z
odt2 = 1979-05-27T00:32:00-07:00
odt3 = 1979-05-27T00:32:00.999999-07:00
出于可读性的目的,你可以用空格替代日期和时刻中间的 T(RFC 3339 的第 5.6 节中允许了这样做)。
odt4 = 1979-05-27 07:32:00Z
小数秒的精度取决于实现,但至少应当能够精确到毫秒。如果它的值超出了实现所支持的精度,那多余的部分必须被舍弃,而不能四舍五入。
如果你省略了 RFC 3339 日期时刻中的时区偏移量,这表示该日期时刻的使用并不涉及时区偏移。在没有其它信息的情况下,并不知道它究竟该被转化成世上的哪一刻。 如果仍被要求转化,那结果将取决于实现。
ldt1 = 1979-05-27T07:32:00
ldt2 = 1979-05-27T00:32:00.999999
如果你只写了 RFC 3339 日期时刻中的日期部分,那它表示一整天,同时也不涉及时区偏移。
ld1 = 1979-05-27
如果你只写了 RFC 3339 日期时刻中的时刻部分,它将只表示一天之中的那个时刻,而与任何特定的日期无关、亦不涉及时区偏移。
lt1 = 07:32:00
lt2 = 00:32:00.999999
数组是内含值的方括号。空白会被忽略。子元素由逗号分隔。子元素的数据类型必须一致(不同写法的字符串应当被认为是相同的类型,不同元素类型的数组也同是数组类型)。
arr1 = [ 1, 2, 3 ]
arr2 = [ "red", "yellow", "green" ]
arr3 = [ [ 1, 2 ], [3, 4, 5] ]
arr4 = [ "所有(写法的)", '字符串', """都是一样的""", '''类型''']
arr5 = [ [ 1, 2 ], ["a", "b", "c"] ]
arr6 = [ 1, 2.0 ] # 有误
数组也可以跨多行。数组的最后一个值后面可以有终逗号(也称为尾逗号)。值和结束括号前可以存在任意数量的换行和注释。
arr7 = [
1, 2, 3
]
arr8 = [
1,
2, # 这是可以的
]
表(也被称为哈希表或字典)是键值对的集合。它们在方括号里,并作为单独的行出现。看得出它们不同于数组,因为数组只有值。
在它下方,直至下一个表或文件结束,都是这个表的键值对。表不保证保持键值对的指定顺序。
[table-1]
key1 = "some string"
key2 = 123
[table-2]
key1 = "another string"
key2 = 456
表名的规则与键名相同(见前文键名定义)。
[dog."tater.man"]
type.name = "pug"
这在 JSON 那儿,是以下结构:
{ "dog": { "tater.man": { "type": { "name": "pug" } } } }
键名周围的空格会被忽略,然而最佳实践还是不要有任何多余的空白。
[a.b.c] # 这是最佳实践
[ d.e.f ] # 等同于 [d.e.f]
[ g . h . i ] # 等同于 [g.h.i]
[ j . "ʞ" . 'l' ] # 等同于 [j."ʞ".'l']
你不必层层完整地写出你不想写的所有途径的父表。TOML 知道该怎么办。
# [x] 你
# [x.y] 不
# [x.y.z] 需要这些
[x.y.z.w] # 来让这生效
空表是允许的,只要里面没有键值对就行了。
类似于键名,你不能重复定义任何表。这样做是错误的。
# 不要这样做
[a]
b = 1
[a]
c = 2
# 也不要这样做
[a]
b = 1
[a.b]
c = 2
行内表提供了一种更为紧凑的语法来表示表,即在一行内表示一个表。行内表由花括号包裹,在括号中,可以出现零或多个逗号分隔的键值对。键值对采取与标准表中键值对相同的形式。什么类型的值都可以,包括行内表。
行内表出现在同一行内。不允许花括号中出现换行,除非它们存在于正确的值当中。即便如此,也强烈不建议把一个行内表搞成纵跨多行的样子。如果你发现自己真的需要,那意味着你应该使用标准表。
name = { first = "汤姆", last = "普雷斯顿—维尔纳" }
point = { x = 1, y = 2 }
animal = { type.name = "哈巴狗" }
# 上述行内表等同于下面的标准表定义
[name]
first = "汤姆"
last = "普雷斯顿—维尔纳"
[point]
x = 1
y = 2
[animal]
type.name = "哈巴狗"
最后还剩下一个没法表示的是表数组。这可以通过双方括号来表示。各个具有相同方括号名的表将会成为该数组内的一员。这些表的出现顺序就是它们的插入顺序。一个没有任何键值对的双方括号表将为视为一个空表。
[[products]]
name = "Hammer"
sku = 738594937
[[products]]
[[products]]
name = "Nail"
sku = 284758393
color = "gray"
这在 JSON 那儿,是以下结构。
{
"products": [
{ "name": "Hammer", "sku": 738594937 },
{ },
{ "name": "Nail", "sku": 284758393, "color": "gray" }
]
}
你还可以创建一个嵌套表数组。只要在子表上使用相同的双方括号语法语法。每个双方括号子表将隶属于上方最近定义的表元素。
[[fruit]]
name = "apple"
[fruit.physical]
color = "red"
shape = "round"
[[fruit.variety]]
name = "red delicious"
[[fruit.variety]]
name = "granny smith"
[[fruit]]
name = "banana"
[[fruit.variety]]
name = "plantain"
上述 TOML 对应下面的 JSON。
{
"fruit": [
{
"name": "apple",
"physical": {
"color": "red",
"shape": "round"
},
"variety": [
{ "name": "red delicious" },
{ "name": "granny smith" }
]
},
{
"name": "banana",
"variety": [
{ "name": "plantain" }
]
}
]
}
若试图向一个静态定义的数组追加内容,即便数组尚且为空或类型兼容,也必须在解析时报错。
# 无效的 TOML 文档
fruit = []
[[fruit]] # 不允许
若试图用已经确定为数组的名称定义表,必须在解析时报错。
# 无效的 TOML 文档
[[fruit]]
name = "apple"
[[fruit.variety]]
name = "red delicious"
# 这个表与之前的表冲突了
[fruit.variety]
name = "granny smith"
你也可以适当使用行内表:
points = [ { x = 1, y = 2, z = 3 },
{ x = 7, y = 8, z = 9 },
{ x = 2, y = 4, z = 8 } ]
以 TOML 表示一个简单的服务配置。
name = "UserProfileServer"
maxconns = 1000
queuecap = 10000
queuetimeout =300
[loginfo]
loglevel = "ERROR"
logsize = "10M"
lognum = 10
logpath = "/usr/local/app/log"
以 Go 为例,解析上面的 TOML 配置文件。
第一步,通过 TOML-to-Go 快速将 TOML 转换为 Go struct。
type Server struct {
Name string `toml:"name"`
Maxconns int `toml:"maxconns"`
Queuecap int `toml:"queuecap"`
Queuetimeout int `toml:"queuetimeout"`
Loginfo struct {
Loglevel string `toml:"loglevel"`
Logsize string `toml:"logsize"`
Lognum int `toml:"lognum"`
Logpath string `toml:"logpath"`
} `toml:"loginfo"`
}
第二步,通过第三方库 BurntSushi/toml 为例完成解析,当然你也可以选择其他自己喜欢的第三方开源库。
package main
import(
"fmt"
"github.com/BurntSushi/toml"
)
type Server struct {
Name string `toml:"name"`
Maxconns int `toml:"maxconns"`
Queuecap int `toml:"queuecap"`
Queuetimeout int `toml:"queuetimeout"`
Loginfo struct {
Loglevel string `toml:"loglevel"`
Logsize string `toml:"logsize"`
Lognum int `toml:"lognum"`
Logpath string `toml:"logpath"`
} `toml:"loginfo"`
}
func main() {
v := Server{}
if _, err := toml.DecodeFile("server.toml", &v); err != nil {
fmt.Printf("parse toml failed, err=%v\n", err)
} else {
fmt.Printf("%+v\n", v)
}
}
运行输出:
{Name:UserProfileServer Maxconns:1000 Queuecap:10000 Queuetimeout:300 Loginfo:{Loglevel:ERROR Logsize:10M Lognum:10 Logpath:/usr/local/app/log}}
面对常见配置文件格式,使用时该如何选择呢?这里给几个选择的原则: (1)支持嵌套结构。仅仅支持 KV 结构的键值对表达能力有点弱; (2)支持注释。不支持注释的 JSON 是给机器读的,不是给人读的; (3)支持不同的数据类型,而不仅仅是 string。这一点,键值对和 XML 表现的非常逊色; (4)最好支持 include 其他配置文件,方便配置模块化。复杂的配置也是无奈之举,但如果支持 include 语法,可以方便的把配置文件模块化。
通过以上几个对配置文件的要求,发现键值对不支持层级关系,JSON 不支持注释,可读性较差,虽然 XML 支持注释和层级结构,且可读性较好,但是因为起始标签一定要有个与之对应的结束标签,文件内容较大,解析时占用较多内存,传输时占用较多带宽。所以这里推荐使用 YAML 和 TOML,很多语言都有其 library 实现,跨语言不成问题。
不同系统、框架和组件可能使用自家自研的配置文件格式,因为其不具有普适性和通用性,这里就不做过多的介绍。比如 Tencent 开源 RPC 框架 TarsGo 采用了类似于 XML 的配置格式,像下面这样:
<tars>
<application>
enableset=n
<server>
node=tars.tarsnode.ServerObj@tcp -h 10.120.129.226 -p 19386 -t 60000
app=TestApp
server=HelloServer
localip=10.120.129.226
</server>
<client>
locator=tars.tarsregistry.QueryObj@tcp -h 10.120.129.226 -p 17890
sync-invoke-timeout=3000
</client>
</application>
</tars>