Post

配置信息管理和配置表生成

配置信息管理和配置表生成

游戏系统中有大量的模块或对象需要开放自定义配置的能力,本篇文章讲下配置表与结构体相互映射的实现。其实配置结构的生成无外乎两种方式:手写Config类和使用代码生成工具。

手写Config类

利用go的注解能力,使得结构体具备序列化成Json对象和反序列化回结构体的能力。具体实现可以使用json包中的MarshalUnmarshal函数。

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文件。

This post is licensed under CC BY 4.0 by the author.