场景
你能从本文中学到什么实际案例 开发
读取 csv 文件预处理表头按照数据填充结构体
技巧使用场景技巧写法麻烦一点的面向对象写法 总结
场景 你能从本文中学到什么- 如何用 go 语言读取 csv 文件(第一行为列名,第二行开始为数据)如何将读取到的数据,填充入任意结构体数据中,类似于
type AnyStruct struct { m map[int32]string s []bool ... } stru:= AnyStruct{} struList, err:=csv.Unmarshal(str,&stru) // struList map[string]AnyStruct{} ...
- 一些使用 reflect 库的实际经验对于单处理函数接口的简化技巧
我们现有的游戏项目采用 UE4 引擎,策划配置的数据以 .uasset 文件格式存储。后端各服务需要读取到指定的游戏数据(例如交易服务需要获取物品列表和价格)。我们采用如下的开发计划:
- 使用 UE4 自带的 Python 环境,编写python脚本脚本自动把指定的 .uasset 转成 .csv (待更新成博客)
注:.uasset 转 .csv 是 UE4 自带功能,支持打开引擎界面后,选中指定 .uasset(可多个)右击输出为 .csv。但是使用必须打开引擎界面,必须每次手动选择,没有命令行可执行、对特定的项目一键输出的脚本
- 后端使用 Go 语言,服务启动时读取需要的 .csv 文件数据,将数据按照自定义结构体的方式存储入内存,方便程序运行时(runtime)取用 (本文内容)后续开发定制化配表工具(C#),直接读取 .uasset 内容,策划也可直接使用定制化配表工具直接修改游戏数据,也能直接一键输出成任意后端可读取格式 (开发中,待更新)
下述所有代码均节选核心逻辑展示,完整项目请查看 csvLoader.
读取 csv 文件import ( "encoding/csv" "os" ) f, err := os.Open(filepath) check(err) // 此处为伪代码,需要对错误进行处理,下同 reader := csv.NewReader(f) preData, err := reader.ReadAll() // preData 数据格式为 [][]string check(err)
至此,我们将 csv 文件保存到一个二维数组 preData 中,第一维是行,第二维是列,例:
源文件 example.csv:
a,b,c 1,2,3 4,5,6
excel 等表格工具打开后:
内存中:
fmt.Println(preData) // [[a b c] [1 2 3] [4 5 6]]预处理表头
type fieldInfo struct { name string // struct field name 结构体字段名 kind FieldKind // 结构体字段类型 index int // 结构体字段在表头的列号 context reflect.Type// 附加类型信息,map、slice等复合类型需要用到 } // 检查处理表头 length := len(preData) - 1 if length < 0 { return nil, nil } allToLower(&preData[0]) // 将表头转化为小写,需求是大小写不敏感 // 提取结构体参数名、类型、对应的位置 vValue := reflect.ValueOf(v).Elem() fieldNum := vValue.NumField() // 获取结构体字段个数 vType := reflect.TypeOf(v).Elem() fieldInfoList := make([]*fieldInfo, fieldNum) for i := 0; i < fieldNum; i++ { //遍历所有结构体字段 field := vType.Field(i) fieldInfoList[i] = &fieldInfo{ name: field.Name, kind: getFieldKind(field.Type.Kind()), // 获取字段类型 index: getFieldIndex(field.Name, &preData[0]), // 获取字段在 csv 中的列号 } if fieldInfoList[i].kind == Map || fieldInfoList[i].kind == Slice { fieldInfoList[i].context = field.Type } }
至此,我们将 csv 文件中的列名,按名称对应到了结构体的字段名称上,并提取了类型等信息(注:结构体中的字段如果未在csv中找到,则跳过不生成,csv中的字段结构体中未找到同理)
按照数据填充结构体data := make(map[string]interface{}, length) for i := 0; i < length; i++ { data[preData[i+1][0]] = reflect.New(vValue.Type()).Interface() // 新建一个给定结构体 setFieldValue(data[preData[i+1][0]], &fieldInfoList, &preData[i+1]) // 填充数据 } return data func setFieldValue(v interface{}, infoList *[]*fieldInfo, data *[]string) { vValue := reflect.ValueOf(v).Elem() for i, info := range *infoList { if info.index > -1 { // string 转化为字段指定类型 value, err := fieldParser[info.kind]((*data)[info.index], info.context) if err == nil { vValue.Field(i).Set(value) // 把动态生成的值写入结构体 } } } }
上述代码中,需要把csv单个单元格中的数据(string 格式),转化为结构体字段的给定格式(int、bool、map、slice 等),这里我使用的是 map[kind]handler 数组。
技巧使用场景当出现如下情形时,使用这种方案高效且易于扩展:
- 大段类似的switch case 代码块
swicth kind { case String: // 处理 case Int8: // 处理 case Int16: // 处理 case Map: // 处理 key swicth key { case String: ... } // 处理 value ... } // 显而意见,这种大段switch很难维护,且在有嵌套(不管是递归、引用还是其他)时,很容易在增加删除一个case后出问题
- 使用事件系统(event handler),且事件和对应的处理函数不在运行时(runtime)发生变化
可以使用 map[kind]handler 数组,把函数作为值进行直接调用,如下:
技巧写法type fieldHandler func(str string, context reflect.Type) (reflect.Value, error) var ( fieldParser = map[FieldKind]fieldHandler { Invalid: stringToInvalid, String: stringToString, Bool: stringToBool, Int: stringToInt, Int8: stringToInt8, Int16: stringToInt16, Int32: stringToInt32, Int64: stringToInt64, Uint: stringToUint, Uint8: stringToUint8, Uint16: stringToUint16, Uint32: stringToUint32, Uint64: stringToUint64, Float32: stringToFloat32, Float64: stringToFloat64, Map: stringToMap, Slice: stringToSlice, } fieldParserHelper map[FieldKind]fieldHandler ) func init() { fieldParserHelper = fieldParser // fieldParserHelper 是在运行时赋值的指针,和 fieldParser 指向同一地址 } func stringToInvalid(_ string, _ reflect.Type) (reflect.Value, error) { return reflect.Value{}, ErrUnsupportedDataType } func stringToString(str string, _ reflect.Type) (reflect.Value, error) { str = strings.Trim(str, """) return reflect.ValueOf(str), nil } ....... // stringToMap 格式 (A:"aaa",B:"ccc"),支持去除任何前后置空格 func stringToMap(str string, context reflect.Type) (reflect.Value, error) { if fieldParserHelper == nil { return reflect.Value{}, ErrInvalidDataSource } str = strings.Trim(str, " ") str = str[1:len(str)-1] strArr := strings.Split(str, ",") result := reflect.MakeMap(context) // reflect 动态生成指定格式的map for _, s := range strArr { s = strings.Trim(s, " ") kv := strings.Split(s, ":") if len(kv) != 2 { continue } // 避免循环引用,顺便检查是否是支持的数据类型,这里可以有嵌套,也能正常执行 keyHandler, ok := fieldParserHelper[getFieldKind(context.Key().Kind())] if !ok { continue } valueKind, ok := fieldParserHelper[getFieldKind(context.Elem().Kind())] if !ok { continue } // 赋值 key, err := keyHandler(kv[0], context.Key()) if err != nil { continue } value, err := valueKind(kv[1], context.Elem()) if err != nil { continue } result.SetMapIndex(key, value) // 动态设置 map 的 key - value } return result, nil } // stringToSlice 格式 ("aaa","ccc"),支持去除任何前后置空格 func stringToSlice(str string, context reflect.Type) (reflect.Value, error) { if fieldParserHelper == nil { return reflect.Value{}, ErrInvalidDataSource } str = strings.Trim(str, " ") str = str[1:len(str)-1] strArr := strings.Split(str, ",") result := reflect.MakeSlice(context, 0, len(strArr)) // reflect 动态生成指定格式的 slice for _, s := range strArr { s = strings.Trim(s, " ") if handler, ok := fieldParserHelper[getFieldKind(context.Elem().Kind())]; ok { value, err := handler(s, context.Elem()) if err != nil { continue } result = reflect.Append(result, value) // 动态增加 slice 的元素 } } return result, nil }
麻烦一点的面向对象写法引入 fieldParserHelper 目的是避免对于 fieldParser 的循环引用,因为 fieldParserHelper 和 fieldParser 都是指向同一个 map 真正地址的指针,而指针可以在运行时再赋值,从而避免了 fieldParser 内函数引用 fieldParser 会导致的循环引用问题。
循环引用链路为 fieldParser — 存储了–> stringToMap 函数 — 函数里会使用–> fieldParser
type fieldParser interface { handler(string, reflect.Type) (reflect.Value, error) } type boolParser struct{} func (bp *boolParser) handler(str string, context reflect.Type) (reflect.Value, error) { // 逻辑代码 } type stringParser struct{} func (sp *stringParser) handler(str string, context reflect.Type) (reflect.Value, error) { // 逻辑代码 } ...
这种标准面向对象写法也能达到需求,且执行效率并不差,但需要额外写不少结构体,这些结构体全局只会生成一次,且只做一个函数的活。
总结本文是在我实现一个具体业务需求时,实现的一个 csv 解析库,他能用在 UE4 数据读取等多种涉及 csv 格式的场景。整个库的代码量约 300 行,且支持所有常用的数据格式解析。完整项目请查看 csvLoader。
如果你有更好的想法或建议,请留言指出,谢谢!
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)