Go語言只知其一;不知其二上手記(一)

1、緣起

  從團隊裏幾個同事在自研的發佈工具中開始用Go語言實現一些模塊,到後來微服務的服務發現工具從Eureka換成了Go語言實現的Consul,雖然本身也一直想早點去了解Go語言,也在考慮將Go語言做爲團隊技術路線中的一部分,無奈瑣事纏身,陸陸續續也就是看了些關於Go的文章。在這個過程當中,Go語言的發展真快,內心的那股吸引也是愈來愈強烈。html

  年前開始,首先在「極客時間」上觀看了《Go語言從入門到實戰 蔡超》的視頻教程,有其餘語言基礎的筒子們能夠拿來看看,55節課從淺到深的講了Go語言的特性。看過以後,忘掉的比記住的要多,也有很多沒有理解的地方,還遠遠不能將Go語言轉換爲生產工具。java

  近日,隨着年後工做步入正軌,也下定決心從使用Go語言來實現手邊的小工具開始,逐步將Go語言用起來。畢竟,在實戰中學習,效果會更好一些,同時也計劃將學習與實戰的過程記錄下來,做爲這段時間的總結,若是能爲我同樣的Go語言新手們帶來一些幫助,那就再好不過了。node

2、小工具(代碼生成工具)的需求

  咱們近期忙碌了一個小程序的項目,後臺用的是 nodejs 的 koa 框架。在設計中,model層的代碼是類似度很高,controller層的代碼也是如此,如controller中的基礎的方法(增、刪、改、基於id查詢對象、分頁查詢多個對象等),那就意味着,model層、controller層的代碼能夠抽象出模板來,經過「代碼生成工具」對「數據字典文件」進行解讀後進行批量的代碼生成。如此,「代碼生成工具」的任務有三個:git

  1. 解析「數據字典」文件
  2. 加載「model模板」、「controller模板」
  3. 根據解析結果,結合模板,生成對應的代碼文件

3、任務1【「解析「數據字典」文件】的實現

一、基礎準備

  • 搭建Go語言編寫環境

  目前,我經常使用的開發工具是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)
    }
    複製代碼

    函數的組成部分:修飾符,函數名,參數,函數體,返回值


  上述內容,已足夠幫助咱們開始小工具的編寫了,固然,過程當中確定會遇到卡殼的現象,這時,充分利用好搜索大法,再加上一點點思考,問題總會迎刃而解的。


二、開始動手

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("寫入文件成功:")
	}
}
複製代碼

4、總結

  上述步驟,從環境配置開始,到讀取excel並逐行逐單元格進行分析,到最後將轉換後的json數據寫入文件,記錄了我學習Go語言的起始過程。代碼比較粗糙,並且,Go語言的不少特性包括優點也遠遠沒有在此體現出來。學路漫漫,在此與對Go語言感興趣的初學者共勉,但願你們在學與用的過程當中,可以逐步的掌握這門神兵利器。

相關文章
相關標籤/搜索