从团队里几个同事在自研的发布工具中开始用Go语言实现一些模块,到后来微服务的服务发现工具从Eureka换成了Go语言实现的Consul,虽然本身也一直想早点去了解Go语言,也在考虑将Go语言做为团队技术路线中的一部分,无奈琐事缠身,陆陆续续也就是看了些关于Go的文章。在这个过程当中,Go语言的发展真快,内心的那股吸引也是愈来愈强烈。html
年前开始,首先在“极客时间”上观看了《Go语言从入门到实战 蔡超》的视频教程,有其余语言基础的筒子们能够拿来看看,55节课从浅到深的讲了Go语言的特性。看过以后,忘掉的比记住的要多,也有很多没有理解的地方,还远远不能将Go语言转换为生产工具。java
近日,随着年后工做步入正轨,也下定决心从使用Go语言来实现手边的小工具开始,逐步将Go语言用起来。毕竟,在实战中学习,效果会更好一些,同时也计划将学习与实战的过程记录下来,做为这段时间的总结,若是能为我同样的Go语言新手们带来一些帮助,那就再好不过了。node
咱们近期忙碌了一个小程序的项目,后台用的是 nodejs 的 koa 框架。在设计中,model层的代码是类似度很高,controller层的代码也是如此,如controller中的基础的方法(增、删、改、基于id查询对象、分页查询多个对象等),那就意味着,model层、controller层的代码能够抽象出模板来,经过“代码生成工具”对“数据字典文件”进行解读后进行批量的代码生成。如此,“代码生成工具”的任务有三个:git
- 解析“数据字典”文件
- 加载“model模板”、“controller模板”
- 根据解析结果,结合模板,生成对应的代码文件
目前,我经常使用的开发工具是vscode、idea,这两个均可以拿来做为编写Go程序,可是vscode要下载插件、要进行配置等等。对于我这么急迫的想上手的人来说,时间是最宝贵的了,因此,我仍是选择了idea体系下的goland,下载即用,省时省力。 固然,要编写Go语言,除了IDE,更主要的前提是安装Go。golang.google.cn/ 上的首页就给出来显眼的按钮“Download Go”,下载安装便可。github
了解Go语言的基本框架结构golang
package main
import "fmt"
func main() {
fmt.Println("你好, 世界!") // 不引入"fmt", 直接使用 println 也是能够的
}
复制代码
同时也知晓了Go语言是如何打印信息的。推荐 tour.go-zh.org/ ,绝佳入门选择。shell
了解Go语言在编写以后的运行与编译方法json
运行小程序
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)
}
复制代码
函数的组成部分:修饰符,函数名,参数,函数体,返回值
上述内容,已足够帮助咱们开始小工具的编写了,固然,过程当中确定会遇到卡壳的现象,这时,充分利用好搜索大法,再加上一点点思考,问题总会迎刃而解的。
a_code_generator
┣━ main
┃ ┗━ main.go
┗━ resource
┗━ datadict.xlsx
package main
func main(){
println("程序运行 @ 开始")
println("程序运行 @ 结束")
}
复制代码
运行结果为(➜ a_code_generator 是当前目录):
➜ a_code_generator go run main/main.go
程序运行 @ 开始
程序运行 @ 结束
复制代码
后续步骤的目标是:实现小工具的第一个任务:“解析“数据字典”文件。
数据字典是xlsx文件,须要使用Go语言实现对xlsx文件的读取。经过搜索,肯定使用 excelize 这个组件,github 地址是 github.com/360EntSecGr…
涉及到组件的使用,首先要考虑如何将第三方组件管理起来,因而,开始搜索Go语言包管理的相关知识。我使用的Go版本是1.13.5,所以可使用 Go Modules 的方式。接着,就须要了解在Goland中是否有相应的使用方式,参考网址为 www.cnblogs.com/xiaobaiskil…
障碍扫除,根据 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("程序运行 @ 结束")
}
复制代码
前一步骤中,文件的地址以及sheet页的名称是咱们写死在程序中的,不够灵活,那咱们如何在程序运行的时候将参数传递到程序内部呢?经过搜索关键字“golang 获取命令行变量”,找到参考,请看 studygolang.com/articles/21… 。用到了第三方模块“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中打印出来的内容就省略掉了
程序运行 @ 结束
复制代码
前一步骤中,咱们能够经过命令行接收参数来打开指定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("程序运行 @ 结束")
}
复制代码
进行到如今,main方法中的代码已经比较长了,并且,明显的分红了一段段的代码块,本着实时重构的态度,咱们接下来能够将这些代码快抽象成函数,以增长程序的可读性,这也是了解函数如何定义的一个很好的阶段。同时,咱们从 f, err := excelize.OpenFile(fileName) 这种代码中,发现了函数是有多个返回值的,并且,最后会返回一个 err,以便咱们针对错误作出响应。这就是咱们模仿的对象。了解error类型,请参照 blog.csdn.net/fwhezfwhez/… 。重构后的代码以下
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的扩充。
// 对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能够记录非空单元格的数量,而后在当前行单元格所有遍历完成以后,再对空行与非空行进行区别处理
- 经此修改后,再运行程序,便只会打印出非空行的非空单元格信息
在咱们的数据字典中,每一个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
}
复制代码
- 经此修改后,再运行程序,便会打印出非空行的非空单元格信息,且标识了所在行的类型
- 处理逻辑与数据字典的格式是一一对应的,若是换一种数据字典格式,则须要进行逻辑调整
- 上述程序中,在某行单元格遍历之初即可知道当前行的类型,意味着咱们能找出每个数据字典的开始,即当前行是标题行时
- 上述程序中,咱们也能找出数据字典(除最后一个)的结束,即当前行是空行且上一行是表头行或内容行时
- 最后一个数据字典的结束,即当前行是内容行且是最后一行时,在上述程序中没有体现出来。由于上述程序在某行单元格遍历以后,对于非空行(非标题行),暂时只能判断出它多是标题行或内容行中的某一种,没办法精肯定性
咱们能够将不一样格式的表格数据,转换成约定的结构化数据,这样,就能够将变化限定在 函数 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处理工具,网址是:github.com/json-iterat…
接下来即可以在遍历过程当中,在不一样的步骤,将不一样的数据转换到不一样的结构上了,主要任务以下:
- 标题行时,建立新的 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: strings.TrimSpace(collectionInfo[1]),
Desc: strings.TrimSpace(collectionInfo[0]),
}
}
// 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)
// 5.可以正常执行到此,说明没有错误,返回 nil
return nil
}
复制代码
在函数 analyzeSheet 中,还用到了其余几个函数,如 setFieldInfo、setCollectionField、addDataDictIntoSlice、printJSON。分别涉及到了switch的用法、map的遍历、slice的使用、slice转json等知识点
经过搜索,参考了 www.jianshu.com/p/30ac7eb57… 上的文章,使用 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语言感兴趣的初学者共勉,但愿你们在学与用的过程当中,可以逐步的掌握这门神兵利器。