好久没有更新博客了,稍微写个几行吧……不然都快不会写东西了 emmm。

众所周知,导出表格是中后台一个非常普遍的需求,而 Golang 官方库中就提供了 encoding/csv

最简单的应用在官方里写的很明白:

package main

import (
	"encoding/csv"
	"log"
	"os"
)

func main() {
	records := [][]string{
		{"first_name", "last_name", "username"},
		{"Rob", "Pike", "rob"},
		{"Ken", "Thompson", "ken"},
		{"Robert", "Griesemer", "gri"},
	}

	w := csv.NewWriter(os.Stdout)

	for _, record := range records {
		if err := w.Write(record); err != nil {
			log.Fatalln("error writing record to csv:", err)
		}
	}

	// Write any buffered data to the underlying writer (standard output).
	w.Flush()

	if err := w.Error(); err != nil {
		log.Fatal(err)
	}
}
复制代码

看上去思路好像没什么问题,我们不是只要依次写入就可以了吗?

似乎是这样,说干就干。

interface{}[]interface{}
func interfaceSliceToStringSlice(dataList []interface{}) (strList []string) {
	for _, data := range dataList {
		strList = append(strList, fmt.Sprintf("%v", data))
	}
	return
}
复制代码

然后,再调用,将表头和每行内容挨个处理,似乎就完事儿了?

func (s *Service) toCsv(ctx context.Context, header []interface{}, dataList [][]interface{}) (result []byte, err error) {
	buffer := bytes.NewBuffer(nil)
	record := csv.NewWriter(buffer)
	if header != nil {
		err = record.Write(interfaceSliceToStringSlice(header))
		if err != nil {
			log.Error("toCsv Header Error: %v", err)
			return
		}
	}
	for _, data := range dataList {
		writeErr := record.Write(interfaceSliceToStringSlice(data))
		if writeErr != nil {
			err = writeErr
			log.Error("toCsv Content Error: %v", err)
			return
		}
	}
	record.Flush()
	if err = record.Error(); err != nil {
		log.Error("toCsv Flush error: %v", err)
		return
	}
	result = buffer.Bytes()
	return
}
复制代码

看上去仿佛是这样,结果运营一用,嘿,Excel 打不开,WPS 和 Numbers 明明都是好好的,在网上查了一下 Excel 乱码的问题,说的是什么默认用的 unicode 解析,而我们打包出来的是 UTF8,编码识别有问题所以不对。当时就很迷惑:UTF8 他喵的不就是一种 unicode 吗?

解决方案其实就是为了做一件事情,那就是压入 BOM 头,BOM 头是一个什么东西呢?——其实就是指定文件编码。

\xef\xbb\xbf

对应的 BOM 头关系可见下表:

BytesEncoding Form
00 00 FE FFUTF-32, big-endian
FF FE 00 00UTF-32, little-endian
FE FFUTF-16, big-endian
FF FEUTF-16, little-endian
EF BB BFUTF-8

说到这里了,既然 BOM 头这么好,为什么不大家都加上 BOM 头呢,就跟 content/type 一样,就再也没有不知道识别成啥的烦恼了,原因有以下几点:

  1. BOM 头增加了无用的空间
  2. 协议限制(主要出现在把 BOM 头识别成字符,还是头上)

基于此,添加 BOM 头还是需要考虑一些兼容性的问题,尤其是如果是在程序之间相互通信的时候,怎么处理 BOM 头或许是一个大难题,还是要因地制宜的去选择「是否添加 BOM 头」。

因此,最终还是加上了一个参数 options,拿来配置是否需要使用 BOM 头,来让函数变得更加通用,适用于不同的场景。

参考资料: