在游戏服务端开发中,我们经常遇到配置表的更新,一种做法是使用存储服务器redis、mongo将数据写入,但是配置表更多的场景是读数据,这样的话,网络IO的损耗很大。为此,我们需要将数据写入内存,并且当配置表发送变化时,可以通知接口变更内存数据。
github.com/spf13/viper
用于读取和监控配置文件
github.com/360EntSecGroup-Skylar/excelize
用于解析excel读取数据和结构
go get -u github.com/spf13/viper
github.com/360EntSecGroup-Skylar/excelize
xlsx, err := excelize.OpenFile(filename)
if err != nil {
panic(err.Error())
}
rows := xlsx.GetRows(sheet1) //rows 类型 [][]string
rows保存有表单sheet1的所有行数据二维字符串数组,使用循环变量即可变量出所有数据。但是,rows保存的都是字符型数据,我们需要在sheet1上增加表行标识数据类型、字段、是否必须等
上面第一行为是否必须(用于标识主键),第二行标识字段类型,第三行标识字段名称, 第四行用于填写注释。
表头解析:
type DataMeta struct { //定义数据元信息:字段名和类型
Name string
DataType string //string, int32, float32
}
type MetaValue map[string]interface{} //定义单行数据字典
type MetaValueCollection []MetaValue //定义单表数据切片
for _, sheet := range sheetSlice {
rows := xlsx.GetRows(sheet)
jsonData[sheet] = MetaValueCollection{}
//获取数据表起始列
validCIndex := make([]bool, 100) //标识可用的列
dataMetaList := make([]*DataMeta,100) //保存数据元信息
for rIndex, row := range rows {
if rIndex == 0 {//required,optional
for cIndex, cell := range row {
if cell == "required" || cell == "optional" {
validCIndex[cIndex] = true
}else{
validCIndex[cIndex] = false
}
}
}else if rIndex == 1 {//数据类型
for cIndex, cell := range row {
if validCIndex[cIndex] == false { //可能存在注释列,跳过
continue
}
//保存数据类型
if dataMetaList[cIndex] == nil {
meta := &DataMeta{DataType:cell}
dataMetaList[cIndex] = meta
}else{
dataMetaList[cIndex].DataType = cell
}
}
}else if rIndex == 2 {//字段名
for cIndex, cell := range row {
if validCIndex[cIndex] == false {
continue
}
//保存数据名
if dataMetaList[cIndex] == nil {
meta := &DataMeta{DataType:cell}
dataMetaList[cIndex] = meta
}else{
dataMetaList[cIndex].Name = cell
}
}
}else if rIndex > 3 {//数据行从5行开始,索引号4
...
}
}
}
完整解析代码参见源码parser.go。(需要源码的请在评论区留言)
运行
go run confbuild/parser.go -filename="./ConfData.xlsm" -sheets="ConfShortWord,ConfChargeGold,ConfWeapon"
生成main.json配置文件,接下来需要对改文件进行监控,当其变化时,修改内存中数据。为此,我们需要生成表对应的go文件
若要存储数据,需要定义:
模板
package confdata
import(
"github.com/spf13/viper"
"sync"
"errors"
)
{{range .}}
type {{.Name}} struct { {{range .Field}}
{{.Name}} {{.DataType}} // {{.NameType}} {{.Comment}}{{end}}
}
var(
{{.Name}}List sync.Map
)
func update{{.Name}}List(){
{{.Name}}Data := viper.Get("{{.Name}}")
{{.Name}}DataTmp, ok := {{.Name}}Data.([]interface{})
if ok == false {
logs.Emergency("ConfShortWord Conf Update failed,reason get json data failed")
}else{
for _, item := range {{.Name}}DataTmp {
itemTmp, ok := item.(map[string]interface{})
if ok == true {
ele := &{{.Name}}{} {{range .Field}} {{if IsString .DataType}}
ele.{{.Name}} = string(itemTmp["{{.Name}}"]) {{else if IsBool .DataType}}
ele.{{.Name}} = bool(itemTmp["{{.Name}}"]) {{else}}
ele.{{.Name}} = {{.DataType}}(itemTmp["{{.Name}}"].(float64)) {{end}}{{end}}
ConfShortWordList.Store(ele.{{.PrimaryKey.Name}}, ele)
}
}
}
}
func FindByPk{{.Name}}(ID {{.PrimaryKey.DataType}}) ({{StrFirstToLower .Name}} *{{.Name}}, err error){
confData, ok := {{.Name}}List.Load(ID)
if ok == false {
err = errors.New("Not Data Found")
return
}
{{StrFirstToLower .Name}}, ok = confData.(*{{.Name}})
if ok == false {
err = errors.New("Struct Error Not {{.Name}}")
return
}
return
}
func All{{.Name}}()(collection []*{{.Name}}, err error){
{{.Name}}List.Range(func(key, value interface{}) bool {
conf, ok := value.(*{{.Name}})
if ok == true {
collection = append(collection, conf)
return true
}else{
return false
}
})
return
}
{{end}}
生成样例:
package confdata
import(
"github.com/spf13/viper"
"sync"
"errors"
)
type ConfShortWord struct {
WordPackId uint32 // required 短语包ID
WordType uint32 // optional 短语类型
Diamond uint32 // optional 购买短语需要的钻石
LiveCoin uint32 // optional 购买短语需要的生存模式金币
RewardCoin uint32 // optional 购买短语需要的赏金模式金币
Medal uint32 // optional 购买短语需要的奖章
StartTime uint32 // optional 开始时间时间戳
EndTime uint32 // optional 结束时间时间戳
Discount uint32 // optional 短语折扣范围内的折扣百分比
VipBuyLimit uint32 // optional 短语购买所需的最低vip类别ID
ValidTime uint32 // optional 短语有效时间戳
Rotate uint32 // optional 自身动作
}
var(
ConfShortWordList sync.Map
)
func updateConfShortWordList(){
ConfShortWordData := viper.Get("ConfShortWord")
ConfShortWordDataTmp, ok := ConfShortWordData.([]interface{})
if ok == false {
logs.Emergency("ConfShortWord Conf Update failed,reason get json data failed")
}else{
for _, item := range ConfShortWordDataTmp {
itemTmp, ok := item.(map[string]interface{})
if ok == true {
ele := &ConfShortWord{}
ele.WordPackId = uint32(itemTmp["WordPackId"].(float64))
ele.WordType = uint32(itemTmp["WordType"].(float64))
ele.Diamond = uint32(itemTmp["Diamond"].(float64))
ele.LiveCoin = uint32(itemTmp["LiveCoin"].(float64))
ele.RewardCoin = uint32(itemTmp["RewardCoin"].(float64))
ele.Medal = uint32(itemTmp["Medal"].(float64))
ele.StartTime = uint32(itemTmp["StartTime"].(float64))
ele.EndTime = uint32(itemTmp["EndTime"].(float64))
ele.Discount = uint32(itemTmp["Discount"].(float64))
ele.VipBuyLimit = uint32(itemTmp["VipBuyLimit"].(float64))
ele.ValidTime = uint32(itemTmp["ValidTime"].(float64))
ele.Rotate = uint32(itemTmp["Rotate"].(float64))
ConfShortWordList.Store(ele.WordPackId, ele)
}
}
}
}
func FindByPkConfShortWord(ID uint32) (confShortWord *ConfShortWord, err error){
confData, ok := ConfShortWordList.Load(ID)
if ok == false {
err = errors.New("Not Data Found")
return
}
confShortWord, ok = confData.(*ConfShortWord)
if ok == false {
err = errors.New("Struct Error Not ConfShortWord")
return
}
return
}
func AllConfShortWord()(collection []*ConfShortWord, err error){
ConfShortWordList.Range(func(key, value interface{}) bool {
conf, ok := value.(*ConfShortWord)
if ok == true {
collection = append(collection, conf)
return true
}else{
return false
}
})
return
}
package confdata
import (
"github.com/spf13/viper"
"fmt"
"github.com/fsnotify/fsnotify"
)
func init() {
viper.SetConfigName("ConfData") // 配置文件名称
viper.SetConfigType("json") //配置文件后缀
viper.AddConfigPath("./confdata/") //配置文件目录
err := viper.ReadInConfig() // 查找和读取配置文件
if err != nil {
panic(fmt.Errorf("Fatal error config file: %s \n", err))
}
viper.WatchConfig() //监控配置文件
viper.OnConfigChange(func(in fsnotify.Event) { //设置处理变化的匿名函数
fmt.Println("Config file changed:", in.Name)
updateConfChargeGoldList() //更新内存中配置表数据
...
})
updateConfChargeGoldList()
...
}
执行export.sh生成结构和配置数据,之后引入配置包confdata, 当配置文件更新时,内存中的配置数据也就更新了。如需完整源码,请在评论区留下邮箱。