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

741 阅读7分钟

一、前言

  在上一篇文章中,介绍了小工具【代码生成工具】中的任务1【解析数据字典文件】实现过程,在实现过程中对Go语言进行了学习,具体学习的内容如下:

  1. 搭建Go语言编写环境
  2. 基本框架结构及编译运行方式
  3. 在控制台打印信息的方式
  4. 定义参数的方式
  5. if 的使用方法
  6. 循环的使用方法
  7. 函数的定义与使用方法
  8. 第三方模块的使用方法
  9. xlsx文件的读取方法(excelize)
  10. 接收命令行参数的实现方式(flag)
  11. 结构体的使用方法
  12. error的定义与使用方法
  13. 去掉首尾空字符的方法( strings.TrimSpace)
  14. Json格式的使用方法(jsoniter)
  15. 将文本输出到文件中(ioutil)

  我们已经生成了schema.json文件,接下来的任务就是将模板加载进来,然后在解析这个schema.json的过程中完成实际代码的生成。

二、任务2、3【加载模板、生成代码文件】的实现

1、基础准备

  审视一下之前的代码,有如下发现:

  • 已经完成了一个相对完整的子功能
  • 根据不同的功能拆分出来了子函数
  • 代码都存在于main.go文件中,文件行数已经比较多了

  那么,在开始新任务之前,最好是将基于不同的函数功能,将单文件拆分成多文件,以增加可读性。新功能的代码,便可以直接再新的对应子文件中进行编写了。

  拆分后的文件如下(终端中执行,➜ a_code_generator 是当前目录):

➜  a_code_generator tree main 
main
├── console.go
├── json.go
├── main.go
├── xlsx.go
└── xlsx2schema.go

  每个文件中包含的内容介绍如下:

  • console.go
    • receiveConsoleParam // 接收控制台变量
  • json.go
    • printJSON // 把 json 打印出来
    • writeWithIoutil // 写入文件
  • xlsx.go
    • 结构体定义
      • DataDict // 数据字典
      • Collection // 数据集合
      • Field // 数据字段
    • listAllSheet // 输出 xlsx 文件中所有的 sheet 页 信息
    • setFieldInfo // 将对应表头的内容设置到Field对应的属性上
    • setCollectionField // 为集合扩充字段
    • addDataDictIntoSlice // 将数据字典加入到集合中
  • xlsx2schema.go
    • analyzeSheet // 遍历xlsx文件中某sheet页逐行逐单元格
    • xlsx2schema // xlsx 内容 转换成 schema 文件
  • main.go
    • main // 入口函数

斜体与加粗的部分是比上一篇文章中有所改变的方法

  main.go 中的 main 方法改造后的结果,如下:

 package main

// 入口函数
func main() {
	println("程序运行 @ 开始")

	// 1.接收控制台变量
	fileName, sheetName, sheetIndex, err := receiveConsoleParam()
	if err != nil {
		println(err.Error())
		return
	}

	// 2.xlsx 文件转换成 schema 文件
	xlsx2schema(fileName, sheetName, sheetIndex)

	println("程序运行 @ 结束")
}

  函数xlsx2schema的方法定义如下:

// xlsx 内容 转换成 schema 文件
func xlsx2schema(fileName string, sheetName string, sheetIndex int) {
	// 1.打开xlsx文件
	f, err := excelize.OpenFile(fileName)
	if err != nil {
		println(err.Error())
		return
	}

	// 2.如果 sheetName 为空 或 sheetIndex 为默认值,则打印出该文件的所有sheet页信息
	if sheetName == "" && sheetIndex == -1 {
		listAllSheet(f)
		return
	}

	// 3.对xlsx文件中的指定名称的sheet页逐行逐单元格进行遍历
	var rows [][]string
	if sheetName != "" { // 3.1.当sheet页名称设置时,以 sheetName 为准
		rows = f.GetRows(sheetName)
	} else { // 3.2.当sheet页名称未设置时,以 sheetIndex 为准
		rows = f.GetRows(f.GetSheetName(sheetIndex))
	}
	err = analyzeSheet(rows)
	if err != nil {
		println(err.Error())
		return
	}
}

  将单文件拆分成多文件之后,程序的运行命令也需要做相应的改变,如下(终端中执行,➜ a_code_generator 是当前目录):

➜  a_code_generator  go run main/* -f ./resource/datadict.xlsx -i 2

  这里涉及到的知识点如下:

  1. Go语言同一package下的多文件实际上和一个文件一样,便量、函数等均可以随意访问
  2. 由于分成了多个文件,在运行或编译main.go时,一定要保证包内的其他文件也都进行了编译,如 go run main/*
  3. 程序运行,必须有一个main包,运行时会去main包下找main入口方法,因此,对于可执行的Go程序,main方法在main包下应该只有一个且不一定非要在main.go文件中

2、开始动手

step_01:设计模板文件

  模板文件实际上就是有两部分组成的,分别是静态内容和动态内容。静态内容是固定不变的部分,动态内容则是在实际生成过程中需要进行替换的部分,这一部分就是模板中的变量,一般会使用专门的模板语言进行编写,由专门的模板引擎进行处理。通过搜索,发现了三个具有参考性的文章,如下:

  通过阅读文章,得知Go语言给我们提供了两个库来处理模板(text/template和html/template),我们要处理的模板是NodeJs的文本,使用 text/template 即可。

  模板示例如下(model.tmpl):

'use strict';
const path = require("path");
const mongoose = require('mongoose');
const CommonDao = require('./commondao.js')
const dbconn = require('./index.js');
const ObjectId = mongoose.Schema.Types.ObjectId;

class Model extends CommonDao {
    constructor(model) { super(model); }

    getSchema() { return Schema; }

    getConn() { return dbconn.mongoConn; }

    getCollect() {
        let file = __filename.split(path.sep);
        file = file[file.length - 1].split('.')[0];
        return file;
    }
}

cont Schema = Model.SchemaExt({

{{.}}

});

module.exports = new Model(null);

最后面的 {{.}} ,便是要替换的内容,只有这一处,因此,没有用不同的变量标识。点"."代表当前作用域的当前对象。

step_02:创建代码文件 template.go

step_03:在 template.go 中添加解析 schema 的方法

  schema文件已经在前一步骤中生成出来了,接下来使用程序将其用程序打开读取文件内容(json),并将内容反向解析成 数据字典集合,然后在集合遍历中进行处理即可。代码框架如下:

// 分析模式文件
func analyzeSchema(schemaPath string, modelTmplPath string, controllerTmplPath string) error {
	// 1.加载 模式文件
	schema, err := ioutil.ReadFile(schemaPath)
	if err != nil {
		return err
	}
	// 2.转换 模式文件到 数据字典集合中
	var dataDictSlice []DataDict // 数据字典集合
	if err := Json.Unmarshal(schema, &dataDictSlice); err != nil {
		return err
	}

	// 3.加载模板文件
	modelTmpl, _ := template.ParseFiles(modelTmplPath)
	controllerTmpl, _ := template.ParseFiles(controllerTmplPath)

	// 4.遍历数据字典集合
	for _, dataDict := range dataDictSlice {

		// 3.1.生成 model 代码片段
		if err := generateModelFile(modelTmpl, dataDict); err != nil {
			return err
		}

		// 3.2.生成 controller 代码片段
		if err := generateControllerFile(controllerTmpl, dataDict); err != nil {
			return err
		}
	}

	// 5.没有出错,返回 nil
	return nil
}

在遍历过程中,用 generateModelFile 和 generateControllerFile 承载了生成代码文件的职责。为了能够复用模板文件,需要将模板文件再遍历前提前加载。analyzeSchema 中涉及到的知识点如下:

  1. 使用 ioutil.ReadFile 读取 schema.json
  2. 使用 Json.Unmarshal 将 json 文本解析为 strcut对象 集合
  3. 使用 template.ParseFiles 加载模板文件(text/template)
  4. 遍历 结构体数组

step_04:在 template.go 中实现 generateModelFile

  在 step_01 中可以看到 model.tmpl 的内容,需要填充的部分就是数据字典下各个字段的定义,因此,我们只需要在程序中遍历数据字典的字段集合,并且根据约定的规则将所有的字段信息转换拼接成代码片段,然后通过模板引擎将其替换到相应的位置后,输出成文件即可。代码框架如下:

// 生成 model 文件
func generateModelFile(modelTmpl *template.Template, dataDict DataDict) error {
	// 1.获取 fields
	fields := dataDict.Fields

	// 2.将 map 的 key 升序处理
	var keys []int
	for key := range fields {
		iKey, err := strconv.Atoi(key)
		if err != nil {
			return err
		}
		keys = append(keys, iKey)
	}
	sort.Ints(keys)

	// 3.顺序遍历并生成 model 代码块
	var modelScript strings.Builder
	for _, key := range keys {
		field := fields[strconv.Itoa(key)]
		// 使用 generateFieldCode 来解析并生成每一个 field 对应的代码
		modelScript.WriteString(generateFieldCode(field))
	}

	// 4.创建代码文件
	modelFile, err := os.Create("./dist/model/" + dataDict.Collection.Name + ".js")
	if err != nil {
		return nil
	}
	
	// 5.模板解析
	modelTmpl.Execute(modelFile, modelScript.String())

	// 6.关闭文件
	defer modelFile.Close()

	// 7.没有错误,则返回 nil
	return nil
}

fields是个 map,是无序的,如果需要 顺序遍历,需要将 key 转存到数组中且进行排序后,通过遍历key数组一一获取 map 中的元素来实现。在遍历过程中,使用 generateFieldCode 来对字段基于约定的规则转换成相应的代码,不同的情况下,此方法的逻辑是不一样的,在此文中就不在叙述了。generateModelFile 中涉及到的知识点如下:

  1. map 的顺序遍历
  2. string类型转int类型 及 int类型转string类型
  3. strings.Builder 的使用方式
  4. 创建文件的方式
  5. 解析模板的方式
  6. defer的使用方法

step_05:在 template.go 中实现 generateFieldCode

  不同的情况下,此方法的逻辑是不一样的,在此文中就不再叙述了。

step_06:在 template.go 中实现 generateControllerFile

  此方法与 generateModelFile 是类似的,在此文中就不再叙述了。

step_07:在 main 方法中,调用 analyzeSchema ,把功能串接起来

三、总结

  结合上一篇文章,到此处,我们已经实现了从读取 xlsx 格式的数据字典开始,到生成 schema 文件,到最后生成代码文件的完整过程。在这个过程中,边查边写的了解了Go语言的一些基本特性,虽然距离熟悉Go语言还相差甚远,但也收获了一个可用的代码生成工具,也算是一个好的开头了。