從團隊裏幾個同事在自研的發佈工具中開始用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語言感興趣的初學者共勉,但願你們在學與用的過程當中,可以逐步的掌握這門神兵利器。