您的浏览器过于古老 & 陈旧。为了更好的访问体验, 请 升级你的浏览器
二周 发布于2019年11月27日 18:18 最近更新于 2019年11月27日 18:20

原创 Go语言查询数据库并导出到Excel

940 次浏览 读完需要≈ 14 分钟 Golang

内容目录

Go语言查询数据库

最近在用Go语言查询数据库的时候发现非常麻烦,我们先来看下官方文档给出的例子:

var (
	id int
	name string
)
rows, err := db.Query("select id, name from users where id = ?", 1)
if err != nil {
	log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
	err := rows.Scan(&id, &name)
	if err != nil {
		log.Fatal(err)
	}
	log.Println(id, name)
}
err = rows.Err()
if err != nil {
	log.Fatal(err)
}

麻烦在哪里:

  1. 需要先定义好接收变量,需要指明正确的类型
  2. rows.Scan的时候需要传递第一步定义的变量的地址
  3. 使用结果的时候无法在循环中进行,需要一个一个的调用,比如上面的log.Println(id, name)

好,这么多问题,肯定有人给出了其他的解决方法,不用这么麻烦的对吧,还是看官方文档,这里给出了一个比较好的解决方法:

cols, err := rows.Columns()
if err != nil {
	// handle the error
} else {
	dest := []interface{}{ // Standard MySQL columns
		new(uint64), // id
		new(string), // host
		new(string), // user
		new(string), // db
		new(string), // command
		new(uint32), // time
		new(string), // state
		new(string), // info
	}
	if len(cols) == 11 {
		// Percona Server
	} else if len(cols) > 8 {
		// Handle this case
	}
	err = rows.Scan(dest...)
	// Work with the values in dest
}

然后呢,如果处理这个dest,官方文档竟然省略了,好吧,我们就自己来处理一下。

可以发现,这个dest是一个指针数组,我们需要拿到其里面的值,也就是做一个转换,从指针数组转成值数组,下面的代码演示了一个综合案例:


func TestQueryMySql(t *testing.T) {
	rows, err := db.Query("select id, apkMd5, apkSize, apkUrl, cpName, createTime, endTime, packageName, softId, type, updateTime, versionCode, versionName from advert_cp")
	if err != nil {
		log.Fatalln(err)
	}

	var rowValues [][]interface{}
	dest := []interface{}{ // Standard MySQL columns
		new(uint64),        // id
		new(string),        // apkMd5
		new(uint64),        // apkSize
		new(string),        // apkUrl
		new(string),        // cpName
		new(time.Time),     // createTime
		new(sql.NullTime),  // endTime
		new(string),        // packageName
		new(sql.NullInt64), // softId
		new(string),        // type
		new(sql.NullTime),  // updateTime
		new(uint64),        // versionCode
		new(string),        // versionName
	}
	for rows.Next() {
		rows.Scan(dest...)
		rowValues = append(rowValues, destValues(dest))
	}
	columns := []string{"id", "apkMd5", "apkSize", "apkUrl", "cpName", "createTime", "endTime", "packageName", "softId", "type", "updateTime", "versionCode", "versionName"}

	util.ExportExcel("广告数据", columns, rowValues)
}

// destValues 转换指针数组为值数组
func destValues(dest []interface{}) []interface{} {
	values := make([]interface{}, len(dest))
	for index, value := range dest {
		var x interface{}
		switch vt := value.(type) {
		// 解析sql.NullXXX的值
		case driver.Valuer:
			if v, err := vt.Value(); err == nil {
				x = v
			} else {
				x = nil
			}
		case *string:
			x = *vt
		case *int64:
			x = *vt
		case *uint64:
			x = *vt
		case *time.Time:
			x = *vt
		default:
			x = vt
		}
		values[index] = x
	}
	return values
}

说下上面的代码:

  1. 可以重复使用dest指针数组
  2. destValues中可以加入自己的逻辑,比如做格式转换,解析sql.NullXXX的值等,日期格式化等
  3. 导出到Excel,下面我就给出导出的代码

导出Excel

导出Excel基本都是用的360开发的Excelize库,这个库是Go 开源项目中唯一支持复杂样式 XLSX 文件的类库,可应用于各种报表系统中。

他提供了很方便的API,可以直接对单元格进行读写。然而在常用的导出中,还是需要写大量的代码,这里我封装了一个简单的工具类

package util

import (
	"fmt"
	"github.com/360EntSecGroup-Skylar/excelize"
	"strconv"
)

// maxCharCount 最多26个字符A-Z
const maxCharCount = 26

// ExportExcel 导出Excel文件
// sheetName 工作表名称
// columns 列名切片
// rows 数据切片,是一个二维数组
func ExportExcel(sheetName string, columns []string, rows [][]interface{}) {
	f := excelize.NewFile()
	sheetIndex := f.NewSheet(sheetName)
	f.DeleteSheet("Sheet1")
	maxColumnRowNameLen := 1 + len(strconv.Itoa(len(rows)))
	columnCount := len(columns)
	if columnCount > maxCharCount {
		maxColumnRowNameLen++
	} else if columnCount > maxCharCount*maxCharCount {
		maxColumnRowNameLen += 2
	}
	columnNames := make([][]byte, 0, columnCount)
	for i, column := range columns {
		columnName := getColumnName(i, maxColumnRowNameLen)
		columnNames = append(columnNames, columnName)
		// 首行
		f.SetCellValue(sheetName, getColumnRowName(columnName, 1), column)
	}
	for rowIndex, row := range rows {
		for columnIndex, columnName := range columnNames {
			// 从第二行开始
			f.SetCellValue(sheetName, getColumnRowName(columnName, rowIndex+2), row[columnIndex])
		}
	}
	f.SetActiveSheet(sheetIndex)
	// Save xlsx file by the given path.
	err := f.SaveAs("./Book1.xlsx")
	if err != nil {
		fmt.Println(err)
	}
}

// getColumnName 生成列名
// Excel的列名规则是从A-Z往后排;超过Z以后用两个字母表示,比如AA,AB,AC;两个字母不够以后用三个字母表示,比如AAA,AAB,AAC
// 这里做数字到列名的映射:0 -> A, 1 -> B, 2 -> C
// maxColumnRowNameLen 表示名称框的最大长度,假设数据是10行,1000列,则最后一个名称框是J1000(如果有表头,则是J1001),是4位
// 这里根据 maxColumnRowNameLen 生成切片,后面生成名称框的时候可以复用这个切面,而无需扩容
func getColumnName(column, maxColumnRowNameLen int) []byte {
	const A = 'A'
	if column < maxCharCount {
		// 第一次就分配好切片的容量
		slice := make([]byte, 0, maxColumnRowNameLen)
		return append(slice, byte(A+column))
	} else {
		// 递归生成类似AA,AB,AAA,AAB这种形式的列名
		return append(getColumnName(column/maxCharCount-1, maxColumnRowNameLen), byte(A+column%maxCharCount))
	}
}

// getColumnRowName 生成名称框
// Excel的名称框是用A1,A2,B1,B2来表示的,这里需要传入前一步生成的列名切片,然后直接加上行索引来生成名称框,就无需每次分配内存
func getColumnRowName(columnName []byte, rowIndex int) (columnRowName string) {
	l := len(columnName)
	columnName = strconv.AppendInt(columnName, int64(rowIndex), 10)
	columnRowName = string(columnName)
	columnName = columnName[:l]
	return
}

希望能帮到你!

  • CodePlayer技术交流群1
  • CodePlayer技术交流群2

0 条评论

撰写评论