【Go】excelize库实现excel导入导出封装(一),自定义导出样式、隔行背景色、自适应行高、动态导出指定列、动态更改表头

news2025/1/5 10:08:36

前言

最近在学go操作excel,毕竟在web开发里,操作excel是非常非常常见的。这里我选择用 excelize 库来实现操作excel。

为了方便和通用,我们需要把导入导出进行封装,这样以后就可以很方便的拿来用,或者进行扩展。

在这里插入图片描述

我参考的是这篇文章:【GO】excelize导入导出封装

功能

这个导入导出封装,除了基本的导入导出,我还需要一些其他功能。例如:设置隔行背景色、自适应行高、忽略指定字段或导出指定字段、复杂表头 等等。

因为实际项目中,操作excel不可能只是导出一个很简单的excel,实际项目中的要求往往要复杂的多。

在这里插入图片描述

导入

导入有以下几个通用的实现

  • 导入单个sheet的数据(已完成)
  • 导入指定sheet的数据(已完成)
  • 导入多个sheet的数据(已完成)

导出

导出呢,就要复杂很多了,一级表头的普通导出是最简单的,实际项目中往往还会有多级表头,然后不管是一级还是多级表头,还需要有各种要求的样式,隔行背景色、自适应行高这种已经算简单的了,复杂点的还有一对多的纵向单元格合并。

所以导出需要实现以下这些:

  • 普通导出(已完成)
    • 一级表头
    • 单个sheet
  • 复杂表头、树形结构表头导出(未完成)
  • 多个sheet导出(未完成)
  • 基于map导出(未完成)
  • 一对多纵向合并单元格(未完成)
  • 动态导出列(已完成)
    • 忽略指定字段
    • 导出指定字段
    • 动态更改表头名称
  • 隔行背景色样式(已完成)
  • 自适应行高样式(已完成)

这篇文章我们就来实现那几个已完成(未完成的还没开始实现呢,还有好多没实现,哭了)

在这里插入图片描述

其实上面这些功能,我之前早就在Java中实现了。感兴趣的话可以去这篇文章看看,有完整代码:

poi+easypoi实现表头多层循环,多级动态表头、树形结构动态表头、纵向合并单元格、多个sheet导出

实现

我们先在项目中,创建一个excel文件夹,里面放的就是我们封装的实现函数

在这里插入图片描述

准备

既然是通用的导入导出,那每次导入导出不同表格时,不可能说写死导入哪些列(列名),而是应该是按照不同表格对应的不同结构体来进行解析数据或导出数据。

所以我们可以定义一个专门用于解析excel的tag结构体(类似于easypoi的@Excel注解),在这个tag结构体定义几个字段,什么表头名称、列下标、列宽啊这些

用的时候呢,就是在不同结构体中,使用反引号去定义 表头名称、列下标、列宽 这些的值。

excel.go

自定义一个tag结构体

package excel

import (
	"github.com/pkg/errors"
	"github.com/xuri/excelize/v2"
	"regexp"
	"strconv"
	"strings"
)

// 定义正则表达式模式
const (
	ExcelTagKey = "excel"
	Pattern     = "name:(.*?);|index:(.*?);|width:(.*?);|needMerge:(.*?);|replace:(.*?);"
)

type ExcelTag struct {
	Value     interface{}
	Name      string // 表头标题
	Index     int    // 列下标(从0开始)
	Width     int    // 列宽
	NeedMerge bool   // 是否需要合并
	Replace   string // 替换(需要替换的内容_替换后的内容。比如:1_未开始 ==> 表示1替换为未开始)
}

// 构造函数,返回一个带有默认值的 ExcelTag 实例
func NewExcelTag() ExcelTag {
	return ExcelTag{
		// 导入时会根据这个下标来拿单元格的值,当目标结构体字段没有设置index时,
		// 解析字段tag值时Index没读到就一直默认为0,拿单元格的值时,就始终拿的是第一列的值
		Index: -1, // 设置 Index 的默认值为 -1
	}
}

定义好了tag结构体,我们还需要给它绑定解析tag的方法

// 读取字段tag值
func (e *ExcelTag) GetTag(tag string) (err error) {
	// 编译正则表达式
	re := regexp.MustCompile(Pattern)
	matches := re.FindAllStringSubmatch(tag, -1)
	if len(matches) > 0 {
		for _, match := range matches {
			for i, val := range match {
				if i != 0 && val != "" {
					e.setValue(match, val)
				}
			}
		}
	} else {
		err = errors.New("未匹配到值")
		return
	}
	return
}

// 设置ExcelTag 对应字段的值
func (e *ExcelTag) setValue(tag []string, value string) {
	if strings.Contains(tag[0], "name") {
		e.Name = value
	}
	if strings.Contains(tag[0], "index") {
		v, _ := strconv.ParseInt(value, 10, 8)
		e.Index = int(v)
	}
	if strings.Contains(tag[0], "width") {
		v, _ := strconv.ParseInt(value, 10, 8)
		e.Width = int(v)
	}
	if strings.Contains(tag[0], "needMerge") {
		v, _ := strconv.ParseBool(value)
		e.NeedMerge = v
	}
	if strings.Contains(tag[0], "replace") {
		e.Replace = value
	}
}

用的时候,比如在某个用户信息结构体中

在这里插入图片描述

自定义一个excel对象结构体

定义好了tag结构体,同样是在 excel.go 文件中,我们还需要一个excel对象结构体,里面有excel file对象、样式等属性,然后再给它绑定设置样式的方法。

type Excel struct {
	F             *excelize.File // excel 对象
	TitleStyle    int            // 表头样式
	HeadStyle     int            // 表头样式
	ContentStyle1 int            // 主体样式1,无背景色
	ContentStyle2 int            // 主体样式2,有背景色
}

// 初始化
func ExcelInit() (e *Excel) {
	e = &Excel{}
	// excel构建
	e.F = excelize.NewFile()
	// 初始化样式
	e.getTitleRowStyle()
	e.getHeadRowStyle()
	e.getDataRowStyle()
	return e
}

// 获取边框样式
func getBorder() []excelize.Border {
	return []excelize.Border{ // 边框
		{Type: "top", Color: "000000", Style: 1},
		{Type: "bottom", Color: "000000", Style: 1},
		{Type: "left", Color: "000000", Style: 1},
		{Type: "right", Color: "000000", Style: 1},
	}
}

// 标题样式
func (e *Excel) getTitleRowStyle() {
	e.TitleStyle, _ = e.F.NewStyle(&excelize.Style{
		Alignment: &excelize.Alignment{ // 对齐方式
			Horizontal: "center", // 水平对齐居中
			Vertical:   "center", // 垂直对齐居中
		},
		Fill: excelize.Fill{ // 背景颜色
			Type:    "pattern",
			Color:   []string{"#fff2cc"},
			Pattern: 1,
		},
		Font: &excelize.Font{ // 字体
			Bold: true,
			Size: 16,
		},
		Border: getBorder(),
	})
}

// 列头行样式
func (e *Excel) getHeadRowStyle() {
	e.HeadStyle, _ = e.F.NewStyle(&excelize.Style{
		Alignment: &excelize.Alignment{ // 对齐方式
			Horizontal: "center", // 水平对齐居中
			Vertical:   "center", // 垂直对齐居中
			WrapText:   true,     // 自动换行
		},
		Fill: excelize.Fill{ // 背景颜色
			Type:    "pattern",
			Color:   []string{"#FDE9D8"},
			Pattern: 1,
		},
		Font: &excelize.Font{ // 字体
			Bold: true,
			Size: 14,
		},
		Border: getBorder(),
	})
}

// 数据行样式
func (e *Excel) getDataRowStyle() {
	style := excelize.Style{}
	style.Border = getBorder()
	style.Alignment = &excelize.Alignment{
		Horizontal: "center", // 水平对齐居中
		Vertical:   "center", // 垂直对齐居中
		WrapText:   true,     // 自动换行
	}
	style.Font = &excelize.Font{
		Size: 12,
	}
	e.ContentStyle1, _ = e.F.NewStyle(&style)
	style.Fill = excelize.Fill{ // 背景颜色
		Type:    "pattern",
		Color:   []string{"#cce7f5"},
		Pattern: 1,
	}
	e.ContentStyle2, _ = e.F.NewStyle(&style)
}

导入

接下来我们就可以来实现导入函数的封装了,在 excel_import.go 文件中

package excel

import (
	"github.com/pkg/errors"
	"github.com/xuri/excelize/v2"
	"go-web/util"
	"reflect"
	"strconv"
)

// ImportExcel 导入数据(单个sheet)
// 需要在传入的结构体中的字段加上tag:excel:"title:列头名称;"
// f 获取到的excel对象、dst 导入目标对象【传指针】
// headIndex 表头的索引,从0开始(用于获取表头名字)
// startRow 头行行数(从第startRow+1行开始扫)
func ImportExcel(f *excelize.File, dst interface{}, headIndex, startRow int) (err error) {
	sheetName := f.GetSheetName(0) // 单个sheet时,默认读取第一个sheet
	err = importData(f, dst, sheetName, headIndex, startRow)
	return
}

// ImportBySheet 导入数据(读取指定sheet)sheetName Sheet名称
func ImportBySheet(f *excelize.File, dst interface{}, sheetName string, headIndex, startRow int) (err error) {
	// 当需要读取多个sheet时,可以通过下面的方式,来调用 ImportBySheet 这个函数
	//sheetList := f.GetSheetList()
	//for _, sheetName := range sheetList {
	//	ImportBySheet(f,dst,sheetName,headIndex,startRow)
	//}
	err = importData(f, dst, sheetName, headIndex, startRow)
	return
}

// 解析数据
func importData(f *excelize.File, dst interface{}, sheetName string, headIndex, startRow int) (err error) {
	rows, err := f.GetRows(sheetName) // 获取所有行
	if err != nil {
		err = errors.New(sheetName + "工作表不存在")
		return
	}
	dataValue := reflect.ValueOf(dst) // 取目标对象的元素类型、字段类型和 tag
	// 判断数据的类型
	if dataValue.Kind() != reflect.Ptr || dataValue.Elem().Kind() != reflect.Slice {
		err = errors.New("Invalid data type")
	}
	heads := []string{}                        // 表头
	dataType := dataValue.Elem().Type().Elem() // 获取导入目标对象的类型信息
	// 遍历行,解析数据并填充到目标对象中
	for rowIndex, row := range rows {
		if rowIndex == headIndex {
			heads = row
		}
		if rowIndex < startRow { // 跳过头行
			continue
		}
		newData := reflect.New(dataType).Elem() // 创建新的目标对象
		// 遍历目标对象的字段
		for i := 0; i < dataType.NumField(); i++ {
			// 这里要用构造函数,构造函数里指定了Index默认值为-1,当目标结构体的tag没有指定index的话,那么 excelTag.Index 就一直为0
			// 那么 row[excelizeIndex] 就始终是 row[0],始终拿的是第一列的数据
			var excelTag = NewExcelTag()
			field := dataType.Field(i) // 获取字段信息和tag
			tag := field.Tag.Get(ExcelTagKey)
			if tag == "" { // 如果tag不存在,则跳过
				continue
			}
			err = excelTag.GetTag(tag)
			if err != nil {
				return
			}
			cellValue := ""
			if excelTag.Index >= 0 { // 当tag里指定了index时,根据这个index来拿数据
				excelizeIndex := excelTag.Index // 解析tag的值
				if excelizeIndex >= len(row) {  // 防止下标越界
					continue
				}
				cellValue = row[excelizeIndex] // 获取单元格的值
			} else { // 否则根据表头名称来拿数据
				if util.IsContain(heads, excelTag.Name) { // 当tag里的表头名称和excel表格里面的表头名称相匹配时
					if i >= len(row) { // 防止下标越界
						continue
					}
					cellValue = row[i] // 获取单元格的值
				}
			}
			// 根据字段类型设置值
			switch field.Type.Kind() {
			case reflect.Int:
				v, _ := strconv.ParseInt(cellValue, 10, 64)
				newData.Field(i).SetInt(v)
			case reflect.String:
				newData.Field(i).SetString(cellValue)
			}
		}
		// 将新的目标对象添加到导入目标对象的slice中
		dataValue.Elem().Set(reflect.Append(dataValue.Elem(), newData))
	}
	return
}

导入这里用到了一个 IsContain 函数,代码如下:

// 判断数组中是否包含指定元素
func IsContain(items interface{}, item interface{}) bool {
	switch items.(type) {
	case []int:
		intArr := items.([]int)
		for _, value := range intArr {
			if value == item.(int) {
				return true
			}
		}
	case []string:
		strArr := items.([]string)
		for _, value := range strArr {
			if value == item.(string) {
				return true
			}
		}
	default:
		return false
	}
	return false
}

导出

excel_export.go 文件中

package excel

import (
	"fmt"
	"github.com/pkg/errors"
	"github.com/xuri/excelize/v2"
	"net/http"
	"reflect"
	"sort"
	"strings"
)

// GetExcelColumnName 根据列数生成 Excel 列名
func GetExcelColumnName(columnNumber int) string {
	columnName := ""
	for columnNumber > 0 {
		columnNumber--
		columnName = string('A'+columnNumber%26) + columnName
		columnNumber /= 26
	}
	return columnName
}

// ================================= 普通导出 =================================

// NormalDownLoad 导出excel并下载(单个sheet)
func NormalDownLoad(fileName, sheet, title string, isGhbj bool, list interface{}, res http.ResponseWriter) error {
	f, err := NormalDynamicExport(list, sheet, title, "", isGhbj, false, nil)
	if err != nil {
		return err
	}
	DownLoadExcel(fileName, res, f)
	return nil
}

// NormalDynamicDownLoad 动态导出excel并下载(单个sheet)
// isIgnore 是否忽略指定字段(true 要忽略的字段 false 要导出的字段)
// fields 选择的字段,多个字段用逗号隔开,最后一个字段后面也要加逗号,如:字段1,字段2,字段3,
// changeHead 要改变表头的字段,格式是{"字段1":"更改的表头1","字段2":"更改的表头2"}
func NormalDynamicDownLoad(fileName, sheet, title, fields string, isGhbj, isIgnore bool,
	list interface{}, changeHead map[string]string, res http.ResponseWriter) error {
	f, err := NormalDynamicExport(list, sheet, title, fields, isGhbj, isIgnore, changeHead)
	if err != nil {
		return err
	}
	DownLoadExcel(fileName, res, f)
	return nil
}

// NormalDynamicExport 导出excel
// ** 需要在传入的结构体中的字段加上tag:excelize:"title:列头名称;index:列下标(从0开始);"
// list 需要导出的对象数组、sheet sheet名称、title 标题、isGhbj 是否设置隔行背景色
func NormalDynamicExport(list interface{}, sheet, title, fields string, isGhbj, isIgnore bool, changeHead map[string]string) (file *excelize.File, err error) {
	e := ExcelInit()
	err = ExportExcel(sheet, title, fields, isGhbj, isIgnore, list, changeHead, e)
	if err != nil {
		return
	}
	return e.F, err
}

// 构造表头(endColName 最后一列的列名 dataRow 数据行开始的行号)
func normalBuildTitle(e *Excel, sheet, title, fields string, isIgnore bool, changeHead map[string]string, dataValue reflect.Value) (endColName string, dataRow int, err error) {
	dataType := dataValue.Type().Elem() // 获取导入目标对象的类型信息
	var exportTitle []ExcelTag          // 遍历目标对象的字段
	for i := 0; i < dataType.NumField(); i++ {
		var excelTag ExcelTag
		field := dataType.Field(i) // 获取字段信息和tag
		tag := field.Tag.Get(ExcelTagKey)
		if tag == "" { // 如果非导出则跳过
			continue
		}
		if fields != "" { // 选择要导出或要忽略的字段
			if isIgnore && strings.Contains(fields, field.Name+",") { // 忽略指定字段
				continue
			}
			if !isIgnore && !strings.Contains(fields, field.Name+",") { // 导出指定字段
				continue
			}
		}
		err = excelTag.GetTag(tag)
		if err != nil {
			return
		}
		// 更改指定字段的表头标题
		if changeHead != nil && changeHead[field.Name] != "" {
			excelTag.Name = changeHead[field.Name]
		}
		exportTitle = append(exportTitle, excelTag)
	}
	// 排序
	sort.Slice(exportTitle, func(i, j int) bool {
		return exportTitle[i].Index < exportTitle[j].Index
	})
	var titleRowData []interface{} // 列头行
	for i, colTitle := range exportTitle {
		endColName := GetExcelColumnName(i + 1)
		if colTitle.Width > 0 { // 根据给定的宽度设置列宽
			_ = e.F.SetColWidth(sheet, endColName, endColName, float64(colTitle.Width))
		} else {
			_ = e.F.SetColWidth(sheet, endColName, endColName, float64(20)) // 默认宽度为20
		}
		titleRowData = append(titleRowData, colTitle.Name)
	}
	endColName = GetExcelColumnName(len(titleRowData)) // 根据列数生成 Excel 列名
	if title != "" {
		dataRow = 3 // 如果有title,那么从第3行开始就是数据行,第1行是title,第2行是表头
		e.F.SetCellValue(sheet, "A1", title)
		e.F.MergeCell(sheet, "A1", endColName+"1") // 合并标题单元格
		e.F.SetCellStyle(sheet, "A1", endColName+"1", e.TitleStyle)
		e.F.SetRowHeight(sheet, 1, float64(30)) // 第一行行高
		e.F.SetRowHeight(sheet, 2, float64(30)) // 第二行行高
		e.F.SetCellStyle(sheet, "A2", endColName+"2", e.HeadStyle)
		if err = e.F.SetSheetRow(sheet, "A2", &titleRowData); err != nil {
			return
		}
	} else {
		dataRow = 2 // 如果没有title,那么从第2行开始就是数据行,第1行是表头
		e.F.SetRowHeight(sheet, 1, float64(30))
		e.F.SetCellStyle(sheet, "A1", endColName+"1", e.HeadStyle)
		if err = e.F.SetSheetRow(sheet, "A1", &titleRowData); err != nil {
			return
		}
	}
	return
}

// 构造数据行
func normalBuildDataRow(e *Excel, sheet, endColName, fields string, row int, isGhbj, isIgnore bool, dataValue reflect.Value) (err error) {
	//实时写入数据
	for i := 0; i < dataValue.Len(); i++ {
		startCol := fmt.Sprintf("A%d", row)
		endCol := fmt.Sprintf("%s%d", endColName, row)
		item := dataValue.Index(i)
		typ := item.Type()
		num := item.NumField()
		var exportRow []ExcelTag
		maxLen := 0 // 记录这一行中,数据最多的单元格的值的长度
		//遍历结构体的所有字段
		for j := 0; j < num; j++ {
			dataField := typ.Field(j) //获取到struct标签,需要通过reflect.Type来获取tag标签的值
			tagVal := dataField.Tag.Get(ExcelTagKey)
			if tagVal == "" { // 如果非导出则跳过
				continue
			}
			if fields != "" { // 选择要导出或要忽略的字段
				if isIgnore && strings.Contains(fields, dataField.Name+",") { // 忽略指定字段
					continue
				}
				if !isIgnore && !strings.Contains(fields, dataField.Name+",") { // 导出指定字段
					continue
				}
			}
			var dataCol ExcelTag
			err = dataCol.GetTag(tagVal)
			fieldData := item.FieldByName(dataField.Name) // 取字段值
			rwsTemp := fieldData.Len()                    // 当前单元格内容的长度
			if rwsTemp > maxLen {                         //这里取每一行中的每一列字符长度最大的那一列的字符
				maxLen = rwsTemp
			}
			// 替换
			if dataCol.Replace != "" {
				split := strings.Split(dataCol.Replace, ",")
				for j := range split {
					s := strings.Split(split[j], "_") // 根据下划线进行分割,格式:需要替换的内容_替换后的内容
					if s[0] == fieldData.String() {
						dataCol.Value = s[1]
					}
				}
			} else {
				dataCol.Value = fieldData
			}
			if err != nil {
				return
			}
			exportRow = append(exportRow, dataCol)
		}
		// 排序
		sort.Slice(exportRow, func(i, j int) bool {
			return exportRow[i].Index < exportRow[j].Index
		})
		var rowData []interface{} // 数据列
		for _, colTitle := range exportRow {
			rowData = append(rowData, colTitle.Value)
		}
		if isGhbj && row%2 == 0 {
			_ = e.F.SetCellStyle(sheet, startCol, endCol, e.ContentStyle2)
		} else {
			_ = e.F.SetCellStyle(sheet, startCol, endCol, e.ContentStyle1)
		}
		if maxLen > 25 { // 自适应行高
			d := maxLen / 25
			f := 25 * d
			_ = e.F.SetRowHeight(sheet, row, float64(f))
		} else {
			_ = e.F.SetRowHeight(sheet, row, float64(25)) // 默认行高25
		}
		if err = e.F.SetSheetRow(sheet, startCol, &rowData); err != nil {
			return
		}
		row++
	}
	return
}


// 下载
func DownLoadExcel(fileName string, res http.ResponseWriter, file *excelize.File) {
	// 设置响应头
	res.Header().Set("Content-Type", "text/html; charset=UTF-8")
	res.Header().Set("Content-Type", "application/octet-stream")
	res.Header().Set("Content-Disposition", "attachment; filename="+fileName+".xlsx")
	res.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
	err := file.Write(res) // 写入Excel文件内容到响应体
	if err != nil {
		http.Error(res, err.Error(), http.StatusInternalServerError)
		return
	}
}

测试

ok,终于写完了导入导出,接下来就是测试啦

在这里插入图片描述

excel_main.go 文件中

package main

import (
	"fmt"
	"github.com/xuri/excelize/v2"
	"go-web/util/excel"
)

func main() {
	//export()
	imports()
}

type Test struct {
	Id       string `excel:"name:用户账号;"`
	Name     string `excel:"name:用户姓名;index:1;"`
	Email    string `excel:"name:用户邮箱;width:25;"`
	Com      string `excel:"name:所属公司;"`
	Dept     string `excel:"name:所在部门;"`
	RoleKey  string `excel:"name:角色代码;"`
	RoleName string `excel:"name:角色名称;replace:1_超级管理员,2_普通用户;"`
	Remark   string `excel:"name:备注;width:40;"`
}

// 导出
func export() {
	var testList = []Test{
		{"fuhua", "符华", "fuhua@123.com", "太虚剑派", "开发部", "CJGLY", "1", "备注备注"},
		{"baiye", "白夜", "baiye@123.com", "天命科技有限公司", "执行部", "PTYG", "2", ""},
		{"chiling", "炽翎", "chiling@123.com", "太虚剑派", "行政部", "PTYG", "2", "备注备注备注备注"},
		{"yunmo", "云墨", "yunmo@123.com", "太虚剑派", "财务部", "CJGLY", "1", ""},
		{"yuelun", "月轮", "yuelun@123.com", "天命科技有限公司", "执行部", "CJGLY", "1", ""},
		{"xunyu", "迅羽",
			"xunyu@123.com哈哈哈哈哈哈哈哈这里是最大行高测试哈哈哈哈哈哈哈哈这11111111111里是最大行高测试哈哈哈哈哈哈哈哈这里是最大行高测试",
			"天命科技有限公司", "开发部", "PTYG", "2",
			"备注备注备注备注com哈哈哈哈哈哈哈哈这里是最大行高测试哈哈哈哈哈哈哈哈这里是最大行高测试哈哈哈哈哈哈哈哈这里是最大行高测里是最大行高测试哈哈哈哈哈哈哈哈这里是最大行高测试"},
	}
	changeHead := map[string]string{"Id": "账号", "Name": "真实姓名"}
	//f, err := excel.NormalExport(testList, "Sheet1", "用户信息", "Id,Email,", true, true, changeHead)
	f, err := excel.NormalDynamicExport(testList, "Sheet1", "用户信息", "", true, false, changeHead)
	if err != nil {
		fmt.Println(err)
		return
	}
	f.Path = "C:\\Users\\Administrator\\Desktop\\测试.xlsx"
	if err := f.Save(); err != nil {
		fmt.Println(err)
		return
	}
}

// 导入
func imports() {
	f, err := excelize.OpenFile("C:\\Users\\Administrator\\Desktop\\测试.xlsx")
	if err != nil {
		fmt.Println("文件打开失败")
	}
	importList := []Test{}
	err = excel.ImportExcel(f, &importList, 1, 2)
	if err != nil {
		fmt.Println(err)
	}
	for _, t := range importList {
		fmt.Println(t)
	}
}

实现效果

然后我们再来看看实现效果,说实话,我觉得这表格还挺好看的哩,不愧是我

在这里插入图片描述

导出

在这里插入图片描述

导入

在这里插入图片描述

最后

这样,我们就实现了一个通用的导入导出工具封装。

上面这些就是全部代码啦,后续等我把剩下几个复杂导出弄完(挖坑…),我会把这些代码全部抽出来,做成一个独立的组件模块,然后上传到Git上,这样以后不管做哪个项目,用的时候直接在go.mod引入就可以啦~完美😁
在这里插入图片描述

好啦,以上就是本篇文章的全部内容了,如果你觉得对你有帮助或者觉得博主写得不错,千万不要吝啬你的大拇指哟(^U^)ノ~YO

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1068400.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

uni-app:服务器端数据绘制echarts图标(renderjs解决手机端无法显示问题)

效果 代码 <template><view click"echarts.onClick" :prop"option" :change:prop"echarts.updateEcharts" id"echarts" class"echarts"></view> </template><script>export default {data()…

MDK(Keil)的MAP文件都记录了啥

1.Section Cross References&#xff1a;译为各区域的交叉参考&#xff0c;记录了每个C文件调用了哪些其它文件的函数&#xff0c;内容很多所以这个部分相当长 2.Removing Unused input sections from the image&#xff1a;移除未使用的输入部分&#xff0c;记录哪些文件的哪些…

Quarto 入门教程 (2):如何使用并编译出不同文档

接着上一期内容&#xff1a;手把手教你使用 Quarto 构建文档 (1)&#xff0c;本文介绍如何使用 Quarto&#xff0c;并编译出文档&#xff08;PDF&#xff0c;MS Word&#xff0c;html&#xff09;等。 安装 根据官方链接&#xff0c;选择适合自己电脑的 Quarto 版本并下载&am…

【开发篇】二十一、SpringBoot整合Kafka

文章目录 1、整合2、消息的生产3、消费4、补充&#xff1a;安装 Kafka主体不是用来做消息中间件的&#xff0c;但也有这个功能&#xff0c;接下来整合Kafka 1、整合 导入依赖坐标&#xff1a; <dependency> <groupId>org.springframework.kafka</groupId&g…

聚观早报 | 微软将推出首款AI芯片;Galaxy S24系列首发

【聚观365】10月8日消息 微软将推出首款AI芯片 Galaxy S24系列首发 特斯拉将发布第三季度财报 台积电9月份营收56亿美元 2023国庆档新片总票房破27亿 微软将推出首款AI芯片 据外媒援引知情人士消息透露&#xff0c;微软计划在下个月举行的年度开发者大会上推出该公司首款…

【PG】Linux系统部署PostgreSQL 单机数据库

安装方式 1 安装包方式 &#xff08;Packages and Installers&#xff09; 支持的操作系统包括 liunxMacosWindowsBSDSolaris 2 源码安装 &#xff08;Source code&#xff09; 下载源码包 通过下载地址PostgreSQL: File Browser 可以看到有各个版本的源码目录 选择13.1…

80、90童年回忆之小霸王游戏机网页版

前言 在如今快节奏的生活中&#xff0c;我们常常追逐着最新潮流&#xff0c;迷失了曾经的经典与回忆。现在&#xff0c;有一种游戏机能够带你回到小时候&#xff0c;让你重温那些令人难以忘怀的游戏时光&#xff0c;这就是小霸王游戏机&#xff01; 作为 80 年代和 90 年代最受…

对于复杂二进制数位dp问题考虑朴素思想:agc015d

https://atcoder.jp/contests/agc015/tasks/agc015_d 我一开始考虑的是直接上二进制数位dp&#xff0c;但发现这很难做 然后其实可以从最朴素的二进制分类讨论角度考虑 同样是那么几个套路&#xff0c;考虑最高位

AXI总线协议基础--几分钟熟悉通道信号和基础架构

目录 一、AXI协议基础 1.1读写通道的基本架构图 1.2猝发操作举例 1.3传输顺序 二、各个通道中的信号描述 2.1全局信号 2.2写地址通道信号 2.3写数据通道信号 2.4写响应通道信号 2.5读地址通道信号 2.6读数据通道 三、通道握手 3.1单一信息传输时的握手过程 3.2不…

NPM 常用命令(九)

目录 1、npm link 1.1 使用语法 1.2 描述 2、npm login 2.1 描述 3、npm logout 3.1 描述 4、npm ls 4.1 使用语法 4.2 描述 5、npm org 5.1 使用语法 5.2 示例&#xff1a; 6、npm outdated 6.1 使用语法 6.2 描述 6.3 示例 7、npm owner 7.1 使用语法 7.2…

使用Spring Initailizr功能~

Spring Initializr是一个用于快速生成Spring Boot项目的在线工具&#xff0c;它允许用户选择所需的Spring Boot版本、添加所需的依赖&#xff0c;并生成一个可用的项目结构&#xff0c;包括基本的配置文件和代码框架&#xff0c;它能够简化Spring Boot项目的初始化过程&#xf…

高通camx开源部分简介

camera整体框架 ISP Pipeline diagram Simple Model Camx and chi_cdk 整体框架 CtsVerifier, Camra Formats Topology of Camera Formats. Topology (USECASE: UsecaseVideo) Nodes List Links between nodes Pipeline PreviewVideo Buffer manager Create Destro…

Centos7安装Redis7.x最新稳定版|配置开机启动(骨灰级|保姆级)

Python3中类的高级语法及实战 Python3(基础|高级)语法实战(|多线程|多进程|线程池|进程池技术)|多线程安全问题解决方案 Python3数据科学包系列(一):数据分析实战 Python3数据科学包系列(二):数据分析实战 Python3数据科学包系列(三):数据分析实战 Win11查看安装的Python路…

数据丢失怎么办?分享3个神仙方法!

“紧急求解答&#xff01;数据丢失了怎么用简单的方法恢复呀&#xff1f;电脑中保存了一些非常重要的数据&#xff0c;不知道怎么就不见了。有没有方法帮我找回来呀&#xff1f;” 在日常办公中&#xff0c;很多用户会将重要的数据保存在电脑中。这样有利于在需要时及时使用文件…

苹果iTunes如何备份数据?按照这几个步骤就可以了!

iTunes是什么&#xff1f;苹果iTunes是辅助用户使用苹果设备的一款功能强大的应用程序。使用过iTunes的苹果用户应该都不陌生&#xff0c;用户可以在iTunes上管理并播放音乐、电影、电视节目等媒体文件。此外&#xff0c;用户还可以通过iTunes来备份手机上的数据。 那么&#…

《低代码指南》——低代码维格云服务菜单

简介​ 快速了解付费客户能够获得维格服务团队哪些服务,本篇内容不包含使用免费试用版本的客户。 了解维格表产品价格与功能权益:戳我看价格与权益​ 客户付费后能得到哪些服务项目?​ 常规服务项目:

jdk21环境配置

1、官方下载jdk http://www.oracle.com/java/ 2、解压指定目录下 将压缩包解压到d:\jdk\jdk-21目录下 3、配置环境变量 windows 10系统配置方法 第一步 计算机 > 属性>高级系统设置 第二步 第三步 新增环境变量 JAVA_HOME 新增环境变量CLASSPATH 第四步 编…

IDEA 自定生成java类文档型注释

IDEA 自定生成java类文档型注释 /*** author 李昊哲* version 1.0.0* create ${DATE} ${TIME}*/

Spotify高质量工程生产力实践

对于大规模团队来说&#xff0c;开发体验将直接影响到产品质量和迭代速度。本文介绍了Spotify工程团队通过打造统一的开发平台&#xff0c;赋能开发团队&#xff0c;实现高质量工程生产力的经验。原文: Quality Engineering Productivity at Spotify 编程可能是一场噩梦。 并不…

SpringBoot 如何使用 JWT 实现身份认证和授权

Spring Boot 使用 JWT 实现身份认证和授权 JSON Web Token&#xff08;JWT&#xff09;是一种用于在网络应用之间安全传递信息的开放标准。它使用了一种紧凑且独立于语言的方式在各方之间传递信息&#xff0c;通常用于在客户端和服务器之间验证用户身份和授权访问资源。本文将…