Go语言一知半解上手记(一)

560 阅读25分钟

一、缘起

  从团队里几个同事在自研的发布工具中开始用Go语言实现一些模块,到后来微服务的服务发现工具从Eureka换成了Go语言实现的Consul,虽然自己也一直想早点去了解Go语言,也在考虑将Go语言作为团队技术路线中的一部分,无奈杂事缠身,陆陆续续也就是看了些关于Go的文章。在这个过程中,Go语言的发展真快,心里的那股吸引也是越来越强烈。

  年前开始,首先在“极客时间”上观看了《Go语言从入门到实战 蔡超》的视频教程,有其他语言基础的筒子们可以拿来看看,55节课从浅到深的讲了Go语言的特性。看过之后,忘掉的比记住的要多,也有不少没有理解的地方,还远远不能将Go语言转换为生产工具。

  近日,随着年后工作步入正轨,也下定决心从使用Go语言来实现手边的小工具开始,逐步将Go语言用起来。毕竟,在实战中学习,效果会更好一些,同时也计划将学习与实战的过程记录下来,作为这段时间的总结,如果能为我一样的Go语言新手们带来一些帮助,那就再好不过了。

二、小工具(代码生成工具)的需求

  我们近期忙碌了一个小程序的项目,后台用的是 nodejs 的 koa 框架。在设计中,model层的代码是相似度很高,controller层的代码也是如此,如controller中的基础的方法(增、删、改、基于id查询对象、分页查询多个对象等),那就意味着,model层、controller层的代码可以抽象出模板来,通过“代码生成工具”对“数据字典文件”进行解读后进行批量的代码生成。如此,“代码生成工具”的任务有三个:

  1. 解析“数据字典”文件
  2. 加载“model模板”、“controller模板”
  3. 根据解析结果,结合模板,生成对应的代码文件

三、任务1【“解析“数据字典”文件】的实现

1、基础准备

  • 搭建Go语言编写环境

  目前,我常用的开发工具是vscode、idea,这两个都可以拿来作为编写Go程序,但是vscode要下载插件、要进行配置等等。对于我这么急迫的想上手的人来讲,时间是最宝贵的了,所以,我还是选择了idea体系下的goland,下载即用,省时省力。   当然,要编写Go语言,除了IDE,更主要的前提是安装Go。golang.google.cn/ 上的首页就给出来显眼的按钮“Download Go”,下载安装即可。

  • 了解Go语言的基本框架结构

    package main
    
    import "fmt"
    
    func main() {
        fmt.Println("你好, 世界!") // 不引入"fmt", 直接使用 println 也是可以的
    }
    

    同时也知晓了Go语言是如何打印信息的。推荐 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 地址是 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("程序运行 @ 结束")
}

step_05:接收命令行参数

  前一步骤中,文件的地址以及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中打印出来的内容就省略掉了
程序运行 @ 结束

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类型,请参照 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的扩充。


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页代表一个业务模块,每个业务模块里,包含多个数据模型,每个数据模型表格,都包含标题行、表头行、内容行,数据模型开始之前,均会有一个空行。 具体格式如下:

table-sample.png

具体代码如下:

// 对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处理工具,网址是:github.com/json-iterat…

  接下来便可以在遍历过程中,在不同的步骤,将不同的数据转换到不同的结构上了,主要任务如下:

  1. 标题行时,创建新的 Collection
  2. 表头行时,收集表头信息,表头名称和列号
  3. 内容行时,收集内容信息,字段内容和列号
  4. 内容行是在表头行之后,因此,通过相同的列号,便能够将表头名称字段内容关联起来了
  5. 内容行在所有非空单元格都遍历之后,就可以将暂存的一行字段内容转换为 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等知识点

step_11:将转换后的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语言感兴趣的初学者共勉,希望大家在学与用的过程中,能够逐步的掌握这门神兵利器。