配置信息管理和配置表生成
游戏系统中有大量的模块或对象需要开放自定义配置的能力,本篇文章讲下配置表与结构体相互映射的实现。其实配置结构的生成无外乎两种方式:手写Config类和使用代码生成工具。
手写Config类
利用go的注解能力,使得结构体具备序列化成Json对象和反序列化回结构体的能力。具体实现可以使用json
包中的Marshal
和Unmarshal
函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import (
"encoding/json"
)
type Config struct {
Name string `json:"name"`
Value int `json:"value"`
}
func (c *Config) ToJSON() ([]byte, error) {
return json.Marshal(c)
}
func (c *Config) FromJSON(data []byte) error {
return json.Unmarshal(data, c)
}
json序列化和反序列化机制
这种能力来源于go的反射机制,通过tag来实现字段与Json键的映射。通过定义结构体的tag,可以在序列化和反序列化时自动处理字段名的转换,简化了代码的编写。结构体的字段具备 json: “field_name” 的tag后,能够使用marshal和unmarshal函数进行自动化的Json转换。
我们要知道marshal是如何将字段转换成json结构的,类型发生了什么变化,这样才能使序列化后的结果是我们需要的。
marshal调用中,json的类型转换规则如下:
- Boolean类型转化为 JSON 的 boolean类型.
- 浮点,整形,数字类型都转化为JSON 的 number类型.
- String 则强制使用UTF-8编码,将无效字节转化为Unicode形式的rune。并且默认使用 HTMLEscape (将 < > & U+2028 U+2029 替换为 \u003c \u003e \u0026 \u2028 \u2029),这个行为可以通过 Encoder 来自定义。
- Array, slice 相应地转化为 JSON array,但是 []byte会编为 base64字符串。空切片则转换为 JSON null 。
- Struct 转为为 JSON object 。每个公开成员(大写开头)都会作为object的一个成员,使用字段名作为键,除非:
- 可以通过 字段tag 来指定名称,作为 object的键;
- 名称后面,可以用逗号分割来附带一些额外的配置;名称可以留空,以保留默认的键,同时附带配置。
- 配置omitempty时,则当字段为空值(零值)的时候忽略这个字段。
- 如果名称指定为-,则总是忽略这个字段。(注意如果想让键名就是-,则要写-,)
- 除了omitempty之外还有个string选项,它会把相应的值以string的形式转化,这只对数字和布尔类型生效。
- 对于匿名字段:
- 如果没有给它指定tag,那么它其中的字段会平铺在父对象中。(译者注:因为Go里没有继承,只有联合,因此一个结构体包含若干个匿名结构体是很常见的,在这种情况下需要平铺,而不是作为子对象)
- 如果指定了tag,则视为一个子对象(译者注:显式指定tag才会视为子对象)
marshal的大致思路是这样的:
首先对传入的对象进行反射,获取到对象的类型信息和字段信息。根据类型信息决定响应的编码方式。首先检查类型是否实现marshalerType,也就是用户自定义的序列化函数,如果实现了就直接调用这个函数进行序列化。如果没有根据类型选择序列化函数。
类型的序列化函数定义在json库中,感兴趣的可以翻看源码。然后使用对应的序列化函数对进行序列化操作,注意要使用字段信息对json字段名等进行配置。
1
Type.Tag.get("json")
反序列化过程和序列化比较类似,unmarshal函数需要两个参数:json数据和目标对象的指针。它会根据json数据中的键值对,自动将值填充到目标对象的对应字段中。这个过程同样依赖于反射机制,通过字段的tag信息来确定如何映射json数据和结构体字段。
1
2
3
4
5
6
7
8
9
10
11
12
import (
"encoding/json"
)
type Config struct {
Name string `json:"name"`
Value int `json:"value"`
}
func (c *Config) FromJSON(data []byte) error {
return json.Unmarshal(data, c)
}
传入空指针或者非指针的话,会返回错误;
它的过程与Marshal相反;它会分配map, slice, 指针 等,按照以下规律:首先检查 JSON null,如果是则把指针设为nil;如果JSON有数据,则将其填入指针所指向的数据内存中;如果指针为空,则会new一个。检查Unmarshaler(包括JSON null),然后如果JSON字段是字符串,则检查TextUnmarshaler。反序列化过程中,先检查JSON的键。如果struct中没有对应的字段,则默认情况下会忽略。
数据转换关系:
- JSON boolean -> bool
- JSON numbers -> float64
- JSON string -> string
- JSON array -> []interface{}
- JSON objects -> map[string]interface{}
- JSON null -> nil
- array转化为切片:先将切片长度设置为0,然后逐个append进去;
- array转化为数组:多出的会被抛弃,不足的会被设为零值;
- object转为map:如果map是nil则会创建一个,如果有旧的map则用旧的map;键的类型必须是string, integer或者实现了json.Unmarshaler或encoding.TextUnmarshaler;
如果一个JSON的值与目标类型不匹配,或者number超出了范围,则会跳过这个值,并继续尽可能地完成剩下的部分。如果后续没有更严重的错误,则会返回UnmarshalTypeError来描述遇到的第一个不匹配的类型。注意,如果出现类型不匹配的情况,那么不保证后续字段都会正常工作。 当解析字符串的值的时候,无效的 utf-8 或者 utf-16 字符不会被视为一种错误;这些无效字符会被替换为 替换字符 U+FFFD
这里对json序列化和反序列化的讲解比较简单,欲了解更加细节的可以自行谷歌。
对于其他的语言,比如c++,也有对应的json库可以实现类似的功能。
定义好配置类的json格式后,就可以通过外部注入json数据来自定义配置。配置文件可以写在本地,在系统启动时加载;也可以通过网络请求从远程服务器获取,实现配置的热加载。
这样设计的缺点是每个需要被配置的结构都需要写一个config类,工作量大,且不易维护。下面讲解一下另外一个方案:使用代码生成工具。
使用代码生成工具
可以直接使用proto来定义配置类的结构,去生成相对应的代码。但是缺点是没有表格,只能向策划开放json编辑器来编辑json文件。