golang text/template实践(二)读取excel数据并生成go文件和json数据文件并使用viper监控文件

概述

在游戏服务端开发中,我们经常遇到配置表的更新,一种做法是使用存储服务器redis、mongo将数据写入,但是配置表更多的场景是读数据,这样的话,网络IO的损耗很大。为此,我们需要将数据写入内存,并且当配置表发送变化时,可以通知接口变更内存数据。

需求分析

  • 读取excel属性和数据
  • 根据excel属性生成struct和对应的读取、更新等方法
  • 根据excel属性和数据生成配置表文件main.json
  • 监控main.json,当文件变化时,更新内存中的配置表数据

要求

github.com/spf13/viper用于读取和监控配置文件
github.com/360EntSecGroup-Skylar/excelize用于解析excel读取数据和结构

go get -u github.com/spf13/viper
github.com/360EntSecGroup-Skylar/excelize

读取excel 表数据

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

go文件生成

若要存储数据,需要定义:

  1. 表属性对应的struct
  2. 带锁的数据集合sync.Map
  3. 更新方法

模板

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
}

viper对配置文件进行读取和监控、更新

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, 当配置文件更新时,内存中的配置数据也就更新了。如需完整源码,请在评论区留下邮箱。

你可能感兴趣的:(golang)