一、缘起
从团队里几个同事在自研的发布工具中开始用Go语言实现一些模块,到后来微服务的服务发现工具从Eureka换成了Go语言实现的Consul,虽然自己也一直想早点去了解Go语言,也在考虑将Go语言作为团队技术路线中的一部分,无奈杂事缠身,陆陆续续也就是看了些关于Go的文章。在这个过程中,Go语言的发展真快,心里的那股吸引也是越来越强烈。
年前开始,首先在“极客时间”上观看了《Go语言从入门到实战 蔡超》的视频教程,有其他语言基础的筒子们可以拿来看看,55节课从浅到深的讲了Go语言的特性。看过之后,忘掉的比记住的要多,也有不少没有理解的地方,还远远不能将Go语言转换为生产工具。
近日,随着年后工作步入正轨,也下定决心从使用Go语言来实现手边的小工具开始,逐步将Go语言用起来。毕竟,在实战中学习,效果会更好一些,同时也计划将学习与实战的过程记录下来,作为这段时间的总结,如果能为我一样的Go语言新手们带来一些帮助,那就再好不过了。
二、小工具(代码生成工具)的需求
我们近期忙碌了一个小程序的项目,后台用的是 nodejs 的 koa 框架。在设计中,model层的代码是相似度很高,controller层的代码也是如此,如controller中的基础的方法(增、删、改、基于id查询对象、分页查询多个对象等),那就意味着,model层、controller层的代码可以抽象出模板来,通过“代码生成工具”对“数据字典文件”进行解读后进行批量的代码生成。如此,“代码生成工具”的任务有三个:
- 解析“数据字典”文件
- 加载“model模板”、“controller模板”
- 根据解析结果,结合模板,生成对应的代码文件
三、任务1【“解析“数据字典”文件】的实现
1、基础准备
- 搭建Go语言编写环境
目前,我常用的开发工具是vscode、idea,这两个都可以拿来作为编写Go程序,但是vscode要下载插件、要进行配置等等。对于我这么急迫的想上手的人来讲,时间是最宝贵的了,所以,我还是选择了idea体系下的goland,下载即用,省时省力。
当然,要编写Go语言,除了IDE,更主要的前提是安装Go。https://golang.google.cn/ 上的首页就给出来显眼的按钮“Download Go”,下载安装即可。
-
了解Go语言的基本框架结构
package main import "fmt" func main() { fmt.Println("你好, 世界!") // 不引入"fmt", 直接使用 println 也是可以的 }
同时也知晓了Go语言是如何打印信息的。推荐 https://tour.go-zh.org/ ,绝佳入门选择。
-
了解Go语言在编写之后的运行与编译方法
运行
go run ./xxx.go
编译
go build ./xxx.go
编译后的文件,直接就可以运行了
-
了解Go语言中定义参数的方法
var fileName string // 声明变量 var sheetIndex int = 1 // 声明变量并初始化,此时也可以忽略参数类型,编译器会自行推导出变量类型 headers := make(map[int]string) // 短变量声明并初始化 // 当然,还有批量变量的声明方式, // 但我们的初心是快速上手实现小工具, // 因此没必要现在就将所有的方式都掌握,先行掌握最规范、最易用的方式即可
同时,也需要了解Go语言中的变量类型,基本类型都是比较好掌握的,更重要的是了解我们经常用的array\map等复杂数据类型,以及json等数据组织方式,当然,Go语言的类型有着自己的特点,也有特有的类型,这些不需要专门去记忆,在用的过程中,变查边用边记忆就好,慢慢地就会越来越熟练的。
-
了解Go语言中使用if的方法
if sheetIndex < 0 { fmt.Println("invild value"); } else if sheetIndex = 1 { fmt.Println(sheetIndex, "the sheet of list"); } else { fmt.Println(sheetIndex, "the sheet of dataDict"); }
最重要的特点是,在 if 关键词之后,在条件语句之前,是可以先执行变量的初始化语句的。当然,这个特性不见得每次都用的上。
-
了解Go语言中使用循环结构的方法
arr := [...]int{6, 2, 4, 9, 8, 3} //1.基本的循环方式 for i := 0; i < len(arr); i++ { fmt.Print(arr[i], "\t") } fmt.Println() //2.range遍历方式 for idx, value := range arr { fmt.Print(idx, "=", value, "\t") } fmt.Println()
通过了解数组遍历的方式去了解循环的使用,最直接了当了。
-
了解Go语言中函数的定义与使用方法
package main import ( "fmt" ) // 函数定义 func sayHi(name string) string { str := "hello, " + name fmt.Println("inner print:", str) return str } // 函数调用 func main() { result := sayHi("world") fmt.Println("outer print:", result) }
函数的组成部分:修饰符,函数名,参数,函数体,返回值
上述内容,已足够帮助我们开始小工具的编写了,当然,过程中肯定会遇到卡壳的现象,这时,充分利用好搜索大法,再加上一点点思考,问题总会迎刃而解的。
2、开始动手
step_01:安装Go以及Goland
step_02:创建项目及代码目录及代码文件
a_code_generator
┣━ main
┃ ┗━ main.go
┗━ resource
┗━ datadict.xlsx
step_03:创建代码框架并初步运行查看结果
package main
func main(){
println("程序运行 @ 开始")
println("程序运行 @ 结束")
}
运行结果为(➜ a_code_generator 是当前目录):
➜ a_code_generator go run main/main.go
程序运行 @ 开始
程序运行 @ 结束
后续步骤的目标是:实现小工具的第一个任务:“解析“数据字典”文件。
step_04:对 xlsx 文件进行逐行逐单元格分析
数据字典是xlsx文件,需要使用Go语言实现对xlsx文件的读取。通过搜索,确定使用 excelize 这个组件,github 地址是 https://github.com/360EntSecGroup-Skylar/excelize。
涉及到组件的使用,首先要考虑如何将第三方组件管理起来,于是,开始搜索Go语言包管理的相关知识。我使用的Go版本是1.13.5,因此可以使用 Go Modules 的方式。接着,就需要了解在Goland中是否有相应的使用方式,参考网址为 https://www.cnblogs.com/xiaobaiskill/p/11819071.html,基于文档中的步骤进行配置即可。
障碍扫除,根据 README.md 的指导,很容易实现我们想要的功能。
组件安装(终端中执行,➜ a_code_generator 是当前目录):
➜ a_code_generator go get github.com/360EntSecGroup-Skylar/excelize
功能代码(main.go中,读取./resource/datadict.xlsx文件中的“总纲”sheet页):
package main
import "github.com/360EntSecGroup-Skylar/excelize"
func main() {
println("程序运行 @ 开始")
// 1.打开xlsx文件
f, err := excelize.OpenFile("./resource/datadict.xlsx")
if err != nil {
println(err.Error())
return
}
// 2.对xlsx文件中的"总纲"sheet页逐行逐单元格进行遍历
rows := f.GetRows("总纲")
for _, row := range rows {
for _, colCell := range row {
print(colCell, "\t")
}
println()
}
println("程序运行 @ 结束")
}
step_05:接收命令行参数
前一步骤中,文件的地址以及sheet页的名称是我们写死在程序中的,不够灵活,那我们如何在程序运行的时候将参数传递到程序内部呢?通过搜索关键字“golang 获取命令行变量”,找到参考,请看 https://studygolang.com/articles/21438 。用到了第三方模块“flag”,能够实现-h,获取帮助,以及通过自定义的flag接收指定参数的功能。
package main
import (
"flag"
"github.com/360EntSecGroup-Skylar/excelize"
)
func main() {
println("程序运行 @ 开始")
// 1.接收控制台变量
var fileName string // xlsx文件路径
var sheetName string // sheet页的名称
flag.StringVar(&fileName, "f", "", "xlsx文件路径")
flag.StringVar(&sheetName, "s", "", "sheet页名称")
flag.Parse()
if fileName == "" || sheetName == "" {
println("请输入xlsx文件路径及sheet页名称,如需帮助,请在命令后输入 -h")
return
}
// 2.打开xlsx文件
f, err := excelize.OpenFile(fileName)
if err != nil {
println(err.Error())
return
}
// 3.对xlsx文件中的指定名称的sheet页逐行逐单元格进行遍历
rows := f.GetRows(sheetName)
for _, row := range rows {
for _, colCell := range row {
print(colCell, "\t")
}
println()
}
println("程序运行 @ 结束")
}
代码中约定了 -f 后面跟着的是“ xlsx 文件路径”,-s 后跟着的是“sheet页名称”
不传递任何参数,运行程序(在终端中运行,➜ a_code_generator 是当前目录):
➜ a_code_generator go run main/main.go
程序运行 @ 开始
请输入xlsx文件路径及sheet页名称,如需帮助,请在命令后输入 -h
命令后输入-h,运行程序(在终端中运行,➜ a_code_generator 是当前目录):
➜ a_code_generator go run main/main.go -h
程序运行 @ 开始
Usage of /var/folders/hw/jyjf138s2vqg0_8sbdwctk000000gn/T/go-build941042187/b001/exe/main:
-f string
xlsx文件路径
-s string
sheet页名称
exit status 2
命令行后输入 -s -f 及相应的值,运行程序(在终端中运行,➜ a_code_generator 是当前目录):
➜ a_code_generator go run main/main.go -f ./resource/datadict.xlsx -s 总纲
程序运行 @ 开始
# 介于篇幅,sheet中打印出来的内容就省略掉了
程序运行 @ 结束
step_06:获取 xlsx 文件中的sheet页信息
前一步骤中,我们可以通过命令行接收参数来打开指定sheet页了,文件名是比较直观可以获得的,但是,sheet页的名称如果忘记了,还得打开文件才能知道,这样有些低效。那么,有没有方法能够让我们通过程序获得sheet页的信息呢?README.md中没有直接给出示例,但是在浏览了github上excelize中的文件后,发现了sheet_test.go,文件的最下面,有个TestGetSheetMap函数,里面正好有我们想要的代码。
package main
import (
"flag"
"github.com/360EntSecGroup-Skylar/excelize"
)
func main() {
println("程序运行 @ 开始")
// 1.接收控制台变量
var fileName string // xlsx文件路径
var sheetName string // sheet页的名称
flag.StringVar(&fileName, "f", "", "xlsx文件路径")
flag.StringVar(&sheetName, "s", "", "sheet页名称")
flag.Parse()
if fileName == "" {
println("请输入xlsx文件路径,如需帮助,请在命令后输入 -h")
return
}
// 2.打开xlsx文件
f, err := excelize.OpenFile(fileName)
if err != nil {
println(err.Error())
return
}
// 3.如果 sheetName 为空,则打印出该文件的所有sheet页信息
if sheetName == "" {
println("该文件中有如下sheet页(没有基于索引排序):")
sheetMap := f.GetSheetMap()
for idx, sheet := range sheetMap {
println("\t", "索引 = ", idx, ", 名称 = ", sheet)
}
return
}
// 4.对xlsx文件中的指定名称的sheet页逐行逐单元格进行遍历,代码没有变化,此处便忽略掉了
println("程序运行 @ 结束")
}
- 如果运行程序时,未使用 -s 输入sheet页名称,则将该文件中的所有 sheet 页信息打印出来
- Go语言中的map是无序的,所以遍历出来的结果并不是顺序的,如果需要顺序输出,则额外需要做一些处理,如将map中的key转存到数组中进行排序后再基于数组遍历map
- 在我们的场景中,一个数据字典的sheet页的数量不会太多,也就没有必要强求顺序输出了
- 通过该功能够获得sheet页索引了,支持通过索引来打开sheet页会更加便捷一些,程序做如下改变
package main
import (
"flag"
"github.com/360EntSecGroup-Skylar/excelize"
)
func main() {
println("程序运行 @ 开始")
// 1.接收控制台变量
var fileName string // xlsx文件路径
var sheetName string // sheet页的名称
var sheetIndex int // sheet页的索引
flag.StringVar(&fileName, "f", "", "xlsx文件路径")
flag.StringVar(&sheetName, "s", "", "sheet页名称,索引和名称使用一个即可,都有值则以名称为准")
flag.IntVar(&sheetIndex, "i", -1, "sheet页索引,索引和名称使用一个即可,都有值则以名称为准")
flag.Parse()
if fileName == "" {
println("请输入xlsx文件路径,如需帮助,请在命令后输入 -h")
return
}
// 2.打开xlsx文件
f, err := excelize.OpenFile(fileName)
if err != nil {
println(err.Error())
return
}
// 3.如果 sheetName 为空 或 sheetIndex 为默认值,则打印出该文件的所有sheet页信息
if sheetName == "" && sheetIndex == -1 {
println("该文件中有如下sheet页(没有基于索引排序):")
sheetMap := f.GetSheetMap()
for idx, sheet := range sheetMap {
println("\t", "索引 = ", idx, ", 名称 = ", sheet)
}
return
}
// 4.对xlsx文件中的指定名称的sheet页逐行逐单元格进行遍历
var rows [][]string
if sheetName != "" { // 4.1.当sheet页名称设置时,以 sheetName 为准
rows = f.GetRows(sheetName)
} else { // 4.2.当sheet页名称未设置时,以 sheetIndex 为准
rows = f.GetRows(f.GetSheetName(sheetIndex))
}
for _, row := range rows {
for _, colCell := range row {
print(colCell, "\t")
}
println()
}
println("程序运行 @ 结束")
}
step_07:在重构中了解函数的定义及error的使用
进行到现在,main方法中的代码已经比较长了,而且,明显的分成了一段段的代码块,本着实时重构的态度,我们接下来可以将这些代码快抽象成函数,以增加程序的可读性,这也是了解函数如何定义的一个很好的阶段。同时,我们从 f, err := excelize.OpenFile(fileName) 这种代码中,发现了函数是有多个返回值的,而且,最后会返回一个 err,以便我们针对错误做出响应。这就是我们模仿的对象。了解error类型,请参照 https://blog.csdn.net/fwhezfwhez/article/details/79175376 。重构后的代码如下
package main
import (
"errors"
"flag"
"github.com/360EntSecGroup-Skylar/excelize"
)
// 接收控制台变量
func receiveConsoleParam() (string, string, int, error) {
var fileName string // xlsx文件路径
var sheetName string // sheet页的名称
var sheetIndex int // sheet页的索引
flag.StringVar(&fileName, "f", "", "xlsx文件路径")
flag.StringVar(&sheetName, "s", "", "sheet页名称,索引和名称使用一个即可,都有值则以名称为准")
flag.IntVar(&sheetIndex, "i", -1, "sheet页索引,索引和名称使用一个即可,都有值则以名称为准")
flag.Parse()
if fileName == "" {
return "", "", -1, errors.New("请输入xlsx文件路径,如需帮助,请在命令后输入 -h")
}
return fileName, sheetName, sheetIndex, nil
}
// 输出xlsx文件中所有的sheet页信息
func listAllSheet(file *excelize.File) {
println("该文件中有如下sheet页(没有基于索引排序):")
sheetMap := file.GetSheetMap()
for idx, sheet := range sheetMap {
println("\t", "索引 = ", idx, ", 名称 = ", sheet)
}
}
// 对xlsx文件中的指定名称的sheet页逐行逐单元格进行遍历
func analyzeSheet(rows [][]string) error {
// 1.合法性校验
if len(rows) <= 0 {
return errors.New("没有需要分析的行")
}
// 2.遍历需要分析的行
for _, row := range rows {
for _, colCell := range row {
print(colCell, "\t")
}
println()
}
// 3.能够正常执行到此,说明没有错误,返回 nil
return nil
}
// 入口函数
func main() {
println("程序运行 @ 开始")
// 1.接收控制台变量
fileName, sheetName, sheetIndex, err := receiveConsoleParam()
if err != nil {
println(err.Error())
return
}
// 2.打开xlsx文件
f, err := excelize.OpenFile(fileName)
if err != nil {
println(err.Error())
return
}
// 3.如果 sheetName 为空 或 sheetIndex 为默认值,则打印出该文件的所有sheet页信息
if sheetName == "" && sheetIndex == -1 {
listAllSheet(f)
return
}
// 4.对xlsx文件中的指定名称的sheet页逐行逐单元格进行遍历
var rows [][]string
if sheetName != "" { // 4.1.当sheet页名称设置时,以 sheetName 为准
rows = f.GetRows(sheetName)
} else { // 4.2.当sheet页名称未设置时,以 sheetIndex 为准
rows = f.GetRows(f.GetSheetName(sheetIndex))
}
err = analyzeSheet(rows)
if err != nil {
println(err.Error())
return
}
println("程序运行 @ 结束")
}
通过以上步骤,我们已经构建好了分析 sheet 页内容的框架,接下来便是实现具体的分析逻辑了,也就是对函数analyzeSheet的扩充。
step_08:函数 analyzeSheet 中 处理空单元格及空行
// 对xlsx文件中的指定名称的sheet页逐行逐单元格进行遍历
func analyzeSheet(rows [][]string) error {
// 1.合法性校验
if len(rows) <= 0 {
return errors.New("没有需要分析的行")
}
// 2.遍历需要分析的行
for rIdx, row := range rows {
notEmptyCellNum := 0 // 本行非空单元格的数量
for cIdx, colCell := range row {
// 去掉单元格内容的首尾空白字符
cellValue := strings.TrimSpace(colCell)
// 如果内容为空,则跳出本次循环
if len(cellValue) <= 0 {
continue
}
if notEmptyCellNum == 0 {
print("行号[", rIdx, "]\t")
}
notEmptyCellNum++
print("列号[", cIdx, "]=", cellValue, "\t")
}
// 遍历完成当前行上的所有单元格以后的操作
if notEmptyCellNum > 0 {
// 当前行存在非空单元格
println()
} else {
// 当前行的所有单元格均无内容
}
}
// 3.能够正常执行到此,说明没有错误,返回 nil
return nil
}
- 通过查询,使用strings.TrimSpace可以将字符串首位的空格字符消除掉
- 遇到单元格内容为空,则跳出当前循环,其后使用notEmptyCellNum可以记录非空单元格的数量,然后在当前行单元格全部遍历完成之后,再对空行与非空行进行区别处理
- 经此修改后,再运行程序,便只会打印出非空行的非空单元格信息
step_09:函数 analyzeSheet 中 确定每一行的类型
在我们的数据字典中,每个sheet页代表一个业务模块,每个业务模块里,包含多个数据模型,每个数据模型表格,都包含标题行、表头行、内容行,数据模型开始之前,均会有一个空行。
具体格式如下:
具体代码如下:
// 对xlsx文件中的指定名称的sheet页逐行逐单元格进行遍历
func analyzeSheet(rows [][]string) error {
// 1.合法性校验
if len(rows) <= 0 {
return errors.New("没有需要分析的行")
}
// 2.逐行逐单元格遍历前的准备工作
currentRowType := 0 // 当前行的类型,0 空行 1 标题行(当前行有一个非空单元格时) 2 表头行 或 内容行(当前行有一个以上非空单元格时)
prevRowType := 0 // 上一行的类型,0 空行 1 标题行(当前行有一个非空单元格时) 2 表头行 或 内容行(当前行有一个以上非空单元格时)
nextRowType := 0 // 下一行的类型,0 空行 或 标题行(当前行是空行时) 1 表头行(当前行为标题行时) 2 内容行(当前行为表头行或内容行时)
// 3.逐行遍历
for _, row := range rows {
// 3.1.逐单元格遍历前的准备工作
notEmptyCellNum := 0 // 当前行非空单元格的数量
currentRowType = 0 // 初始化当前行类型为默认值 0 即 空行
// 3.2.遍历当前行上的所有单元格
for cIdx, colCell := range row {
// 3.2.1.去掉单元格内容的首尾空白字符
cellValue := strings.TrimSpace(colCell)
// 3.2.2.如果内容为空,则跳出本次循环
if len(cellValue) <= 0 {
continue
}
// 3.2.3.如果内容不为空,则进行数据处理(prevRowType 是真正的上一行的类型,nextRowType是在最后计算的,在此处使用,实际上就代表当前行的类型,是推断值)
if nextRowType == 1 && prevRowType == 1 {
// 3.2.3.1.当前行是表头行,且,上一行是标题行
print("[表头行]列号[", cIdx, "]=", cellValue, "\t")
} else if nextRowType == 2 && prevRowType == 2 {
// 3.2.3.2.当前行是内容行,且,上一行是表头行或内容行
print("[内容行]列号[", cIdx, "]=", cellValue, "\t")
} else if nextRowType == 0 && prevRowType == 0 {
// 3.2.3.3.当前行是空行或标题行(此处不可能是空行,因为这里是内容不为空时才能执行到,则当前行只能是标题行)
print("[标题行]列号[", cIdx, "]=", cellValue, "\t")
}
// 3.2.4.更新当前行不为空的单元格的数量,后面会用来判断当前行是标题行(单元格合并之后只会有一个非空单元格)还是 表头行或内容行(单元格最多8个非空内容,最少5个)
notEmptyCellNum++
// 3.2.5.判断当前行的类型
if notEmptyCellNum == 1 {
// 当前行非空单元格数量为1时,可能是 标题行
// 如果是 标题行,则后续当前行循环时要么是没有单元格了,要么就是空的单元格,是不会执行else的,也就保证了该值停留在本次的赋值中
// 如果是 表头行或内容行,则后续当前行循环时,还会有分控单元格,会执行 else 逻辑,将该值覆盖掉的
currentRowType = 1
} else {
// 当前行非空单元格数量不为1时,不为1,肯定就是比1大了,说明 是 表头行或内容行
currentRowType = 2
}
}
// 3.3.遍历完成当前行上的所有单元格以后的操作
if notEmptyCellNum > 0 {
// 3.3.1.当前行存在非空单元格
if currentRowType == 1 {
// 当前行为标题行时,下一行预测为表头行
nextRowType = 1
} else if currentRowType == 2 {
// 当前行为表头行或内容行时,下一行 预测为 内容行
nextRowType = 2
} else {
// 当前行为空行时,下一行为空行或标题行,其实这里永远不会执行,因为空行会在父id对应的else中
nextRowType = 0
}
// 3.3.2.打印空行
println()
} else {
// 3.3.2.当前行的所有单元格均无内容,即空行
if prevRowType == 2 {
// 当前行为空行,但上一行为表头行或内容行时,表示此时是一个数据字典的结束,而且肯定不是最后一个数据字典
// 多打印几个换行,将内容隔开
print("\n\n\n")
}
// 重置 当前行的类型 及 下一行的类型
currentRowType = 0
nextRowType = 0
}
// 3.4.当前行的循环结束,将当前行类型赋值给到上一行类型,因为接下来就是下一行的分析了
prevRowType = currentRowType
}
// 4.能够正常执行到此,说明没有错误,返回 nil
return nil
}
- 经此修改后,再运行程序,便会打印出非空行的非空单元格信息,且标识了所在行的类型
- 处理逻辑与数据字典的格式是一一对应的,如果换一种数据字典格式,则需要进行逻辑调整
- 上述程序中,在某行单元格遍历之初便可知道当前行的类型,意味着我们能找出每一个数据字典的开始,即当前行是标题行时
- 上述程序中,我们也能找出数据字典(除最后一个)的结束,即当前行是空行且上一行是表头行或内容行时
- 最后一个数据字典的结束,即当前行是内容行且是最后一行时,在上述程序中没有体现出来。因为上述程序在某行单元格遍历之后,对于非空行(非标题行),暂时只能判断出它可能是标题行或内容行中的某一种,没办法精确定性
step_10:函数 analyzeSheet 中 在遍历时将表格数据整理成结构化数据
我们可以将不同格式的表格数据,转换成约定的结构化数据,这样,就可以将变化限定在 函数 analyzeSheet 内,进而保证后续处理程序的一致性。 对于结构化数据,java中有类来表示,而Go语言则提供了结构体。我们的数据字典针是MongoDB的,因此,我们设计了如下的结构体:
// 数据字典
type DataDict struct {
Collection Collection `json:"collection"`
Fields map[string]Field `json:"Fields"`
}
// 数据集合
type Collection struct {
Name string `json:"name"`
Desc string `json:"desc"`
}
// 数据字段
type Field struct {
No string `json:"no"`
Name string `json:"name"`
Desc string `json:"desc"`
Type string `json:"type"`
IsCanBeNul bool `json:"isCanBeNul"`
DefaultValue string `json:"defaultValue"`
VerifyRule string `json:"verifyRule"`
Memo string `json:"memo"`
}
对于数据的表现形式,自然还是想到了json,上述程序中的”json“字样便是为其准备的,网上搜索一番之后,选定了 jsoniter 最为json处理工具,网址是:https://github.com/json-iterator/go。
接下来便可以在遍历过程中,在不同的步骤,将不同的数据转换到不同的结构上了,主要任务如下:
- 标题行时,创建新的 Collection
- 表头行时,收集表头信息,表头名称和列号
- 内容行时,收集内容信息,字段内容和列号
- 内容行是在表头行之后,因此,通过相同的列号,便能够将表头名称字段内容关联起来了
- 内容行在所有非空单元格都遍历之后,就可以将暂存的一行字段内容转换为 Field 结构体了
变化的代码部分如下:
// 将对应表头的内容设置到Field对应的属性上
func setFieldInfo(field *Field, title string, value string) error {
switch title {
case "序号":
field.No = value
case "名称":
field.Name = value
case "描述":
field.Desc = value
case "类型":
field.Type = value
case "是否可空":
if value == "是" {
field.IsCanBeNul = true
} else {
field.IsCanBeNul = false
}
case "校验规则":
field.VerifyRule = value
case "默认值":
field.DefaultValue = value
case "备注":
field.Memo = value
default:
return errors.New("字段不需要该信息:" + title)
}
return nil
}
// 为集合扩充字段
func setCollectionField(headers map[int]string, field map[int]string) Field {
var returnField Field
for idx, info := range field {
err := setFieldInfo(&returnField, headers[idx], info)
if err != nil {
println("error: ", err)
}
}
return returnField
}
// 将数据字典加入到集合中
func addDataDictIntoSlice(collection Collection, fields map[string]Field, dataDictSlice []DataDict) []DataDict {
dataDict := DataDict{
Collection: collection,
Fields: fields,
}
return append(dataDictSlice, dataDict)
}
var Json = jsoniter.ConfigCompatibleWithStandardLibrary
// 把json打印出来
func printJSON(content interface{}) {
c, err := Json.MarshalIndent(content, "", " ")
if err != nil {
println("error: ", err)
}
println(string(c))
}
// 对xlsx文件中的指定名称的sheet页逐行逐单元格进行遍历
func analyzeSheet(rows [][]string) error {
// 1.合法性校验
if len(rows) <= 0 {
return errors.New("没有需要分析的行")
}
// 2.逐行逐单元格遍历前的准备工作
currentRowType := 0 // 当前行的类型,0 空行 1 标题行(当前行有一个非空单元格时) 2 表头行 或 内容行(当前行有一个以上非空单元格时)
prevRowType := 0 // 上一行的类型,0 空行 1 标题行(当前行有一个非空单元格时) 2 表头行 或 内容行(当前行有一个以上非空单元格时)
nextRowType := 0 // 下一行的类型,0 空行 或 标题行(当前行是空行时) 1 表头行(当前行为标题行时) 2 内容行(当前行为表头行或内容行时)
maxRowIndex := len(rows) - 1 // 最大的行索引,索引从 0 开始
var dataDictSlice []DataDict // 数据字典集合
var collection Collection // 数据集合
var fields map[string]Field // 字段集合
headers := make(map[int]string) // 表头集合
// 3.逐行遍历
for rIdx, row := range rows {
// 3.1.逐单元格遍历前的准备工作
notEmptyCellNum := 0 // 当前行非空单元格的数量
currentRowType = 0 // 初始化当前行类型为默认值 0 即 空行
fieldInfo := make(map[int]string) // 存储字段的信息
// 3.2.遍历当前行上的所有单元格
for cIdx, colCell := range row {
// 3.2.1.去掉单元格内容的首尾空白字符
cellValue := strings.TrimSpace(colCell)
// 3.2.2.如果内容为空,则跳出本次循环
if len(cellValue) <= 0 {
continue
}
// 3.2.3.如果内容不为空,则进行数据处理(prevRowType 是真正的上一行的类型,nextRowType是在最后计算的,在此处使用,实际上就代表当前行的类型,是推断值)
if nextRowType == 1 && prevRowType == 1 {
// 3.2.3.1.当前行是表头行,且,上一行是标题行,这时需要收集的是:字段名与列号信息
print("[表头行]列号[", cIdx, "]=", cellValue, "\t")
headers[cIdx] = colCell
} else if nextRowType == 2 && prevRowType == 2 {
// 3.2.3.2.当前行是内容行,且,上一行是表头行或内容行,这时需要收集的是:某个字段的某一个信息(如字段名)与列号信息
print("[内容行]列号[", cIdx, "]=", cellValue, "\t")
fieldInfo[cIdx] = colCell
} else if nextRowType == 0 && prevRowType == 0 {
// 3.2.3.3.当前行是空行或标题行(此处不可能是空行,因为这里是内容不为空时才能执行到,则当前行只能是标题行),这时需要收集的是:数据集合的信息
print("[标题行]列号[", cIdx, "]=", cellValue, "\t")
collectionInfo := strings.Split(cellValue, "|")
collection = Collection{
Name: collectionInfo[0],
Desc: collectionInfo[1],
}
}
// 3.2.4.更新当前行不为空的单元格的数量,后面会用来判断当前行是标题行(单元格合并之后只会有一个非空单元格)还是 表头行或内容行(单元格最多8个非空内容,最少5个)
notEmptyCellNum++
// 3.2.5.判断当前行的类型
if notEmptyCellNum == 1 {
// 当前行非空单元格数量为1时,可能是 标题行
// 如果是 标题行,则后续当前行循环时要么是没有单元格了,要么就是空的单元格,是不会执行else的,也就保证了该值停留在本次的赋值中
// 如果是 表头行或内容行,则后续当前行循环时,还会有分控单元格,会执行 else 逻辑,将该值覆盖掉的
currentRowType = 1
} else {
// 当前行非空单元格数量不为1时,不为1,肯定就是比1大了,说明 是 表头行或内容行
currentRowType = 2
}
}
// 3.3.遍历完成当前行上的所有单元格以后的操作
if notEmptyCellNum > 0 {
// 3.3.1.当前行存在非空单元格
if currentRowType == 1 {
// 当前行为标题行时,下一行预测为表头行
nextRowType = 1
fields = make(map[string]Field)
} else if currentRowType == 2 {
// 当前行为表头行或内容行时,下一行 预测为 内容行
nextRowType = 2
// 如果 fieldInfo 中没有内容,表明当前行肯定是表头行;如果 fieldInfo 中有内容,表明本行肯定是内容行,需要将每个单元格收集到的信息转换成字段对象并加入到 fields 中
if len(fieldInfo) > 0 {
field := setCollectionField(headers, fieldInfo)
fields[field.No] = field
// 如果,当前行是内容行 且 是最后一行时,表示此时是最后一个数据字典的结束,将数据字典组装好之后加入到 切片 中
if rIdx == maxRowIndex {
dataDictSlice = addDataDictIntoSlice(collection, fields, dataDictSlice)
}
}
} else {
// 当前行为空行时,下一行为空行或标题行,其实这里永远不会执行,因为空行会在父id对应的else中
nextRowType = 0
}
// 3.3.2.打印空行
println()
} else {
// 3.3.2.当前行的所有单元格均无内容,即空行
if prevRowType == 2 {
// 当前行为空行,但上一行为表头行或内容行时,表示此时是一个数据字典的结束,而且肯定不是最后一个数据字典
// 多打印几个换行,将内容隔开
print("\n\n\n")
// 组装好数据字典,将数据字典加入到 切片 中
dataDictSlice = addDataDictIntoSlice(collection, fields, dataDictSlice)
}
// 重置 当前行的类型 及 下一行的类型
currentRowType = 0
nextRowType = 0
}
// 3.4.当前行的循环结束,将当前行类型赋值给到上一行类型,因为接下来就是下一行的分析了
prevRowType = currentRowType
}
// 4.打印转换后的json数据
printJSON(dataDictSlice)
// 4.能够正常执行到此,说明没有错误,返回 nil
return nil
}
在函数 analyzeSheet 中,还用到了其他几个函数,如 setFieldInfo、setCollectionField、addDataDictIntoSlice、printJSON。分别涉及到了switch的用法、map的遍历、slice的使用、slice转json等知识点
step_11:将转换后的json文本输出到文件中
通过搜索,参考了 https://www.jianshu.com/p/30ac7eb57bce 上的文章,使用 ioutil 来实现文件输出,具体改动部分如下:
// 把json打印出来
func printJSON(content interface{}) {
c, err := Json.MarshalIndent(content, "", " ")
if err != nil {
println("error: ", err)
}
println(string(c))
WriteWithIoutil("schema.json", string(c))
}
// 写入文件
func WriteWithIoutil(name, content string) {
data := []byte(content)
if ioutil.WriteFile(name, data, 0644) == nil {
println("写入文件成功:")
}
}
四、总结
上述步骤,从环境配置开始,到读取excel并逐行逐单元格进行分析,到最后将转换后的json数据写入文件,记录了我学习Go语言的起始过程。代码比较粗糙,而且,Go语言的很多特性包括优势也远远没有在此体现出来。学路漫漫,在此与对Go语言感兴趣的初学者共勉,希望大家在学与用的过程中,能够逐步的掌握这门神兵利器。