最近项目中有个小需求,需要将查询结果导出到excel。之间前java比较容易,使用POI很容易就能实现,查了下golang的文档,发现golang下边并没有导出excel的包,但是却有一个encoding/csv的包,看了下发现可以导出csv文件,大家都知道csv文件其实就是文本格式的excel文件,可以直接通过excel打开或是导入excel。
看起来挺好的,问题如愿解决,但是事实证明对已一个还不成熟的语言或是库最好还是先测一下的好。兴冲冲的卸了测试例子,成功导出了一个text.csv文件,一切看起来都挺好的,然而打开之后就傻眼了:中文乱码,这个问题其实比较好理解,golang只支持utf-8,而win中文版默认字符集是GB2312(gbk),这样看来直接转码就行了呗。
由于之前吃了亏,这次我们先直接报文件转下码试试:直接将之前导出的text.csv另存为ASCII格式,打开后发现回车换行符丢了,全部变成一行了。这下就郁闷了,先看下源码吧:
// Writer writes a single CSV record to w along with any necessary quoting.
// A record is a slice of strings with each string being one field.
func (w *Writer) Write(record []string) (err error) {
for n, field := range record {
if n > 0 {
if _, err = w.w.WriteRune(w.Comma); err != nil {
return
}
}
// If we don't have to have a quoted field then just
// write out the field and continue to the next field.
if !w.fieldNeedsQuotes(field) {
if _, err = w.w.WriteString(field); err != nil {
return
}
continue
}
if err = w.w.WriteByte('"'); err != nil {
return
}
for _, r1 := range field {
switch r1 {
case '"':
_, err = w.w.WriteString(`""`)
case '\r':
if !w.UseCRLF {
err = w.w.WriteByte('\r')
}
case '\n':
if w.UseCRLF {
_, err = w.w.WriteString("\r\n")
} else {
err = w.w.WriteByte('\n')
}
default:
_, err = w.w.WriteRune(r1)
}
if err != nil {
return
}
}
if err = w.w.WriteByte('"'); err != nil {
return
}
}
if w.UseCRLF {
_, err = w.w.WriteString("\r\n")
} else {
err = w.w.WriteByte('\n')
}
return
}
可以看到代码十分简单,每行就是按照csv的格式写入文件而已。需要注意的是writer里边有一个UserCRLF来指定是否适应回车换行符,默认为false,问题应该就出在这里,但是将UserCRLF设置为true之后,问题依旧,看来是转码有问题。
既然代码这么简单,那么还不如直接自己实现,然后转码输出,这里使用iconv-go进行转码,实现如下:
package components
import (
"bytes"
"errors"
iconv "github.com/djimenez/iconv-go"
)
/**
* 导出处理
*/
const (
OUT_ENCODING = "gbk" //输出编码
)
/**
* 导出csv格式文件,输出byte数组
* 输出编码通过OUT_ENCODING指定
*/
func ExportCsv(head []string, data [][]string) (out []byte, err error) {
if len(head) == 0 {
err = errors.New("ExportCsv Head is nil")
return
}
columnCount := len(head)
dataStr := bytes.NewBufferString("")
//添加头
for index, headElem := range head {
separate := ","
if index == columnCount-1 {
separate = "\n"
}
dataStr.WriteString(headElem + separate)
}
//添加数据行
for _, dataArray := range data {
if len(dataArray) != columnCount { //数据项数小于列数
err = errors.New("ExportCsv data format is error.")
}
for index, dataElem := range dataArray {
separate := ","
if index == columnCount-1 {
separate = "\n"
}
dataStr.WriteString(dataElem + separate)
}
}
//处理编码
out = make([]byte, len(dataStr.Bytes()))
iconv.Convert(dataStr.Bytes(), out, "utf-8", OUT_ENCODING)
return
}
测试一下,导出成功,而且没有乱码问题。
对于目前这个项目而言,导出简单格式的csv就能满足,但是如果想导出复杂的excel文件就不行了。考虑了下大概想出了以下几种方法:
- 使用cgo,用c来实现导出,这是golang处理类似问题的一贯作风,然而就导出excel而言并不太好,因为c导出复杂格式的excel本身就挺麻烦的
- 调用其他语言实现的模块,至于怎么调就无所谓,如果对于大批量的导出,其实还挺好的,可以把生成excel这样费时费力的操作给分离出去
- 按照excel文件格式直接生成为对应二进制文件,这个实现起来可能比较费劲,但是确实一劳永逸的,这里附一个excel格式的链接,有兴趣的可以实现下。