Golangでcsvの要素をダブルクオートでくくる

## やりたいこと

[CSVファイルの一般的書式 (RFC4180 日本語訳)](http://www.kasai.fm/wiki/rfc4180jp)より

>(レコード中の) 各フィールドは、それぞれダブルクォーテーションで囲んでも囲わなくてもよい

とあるので、プロジェクトによってはダブルクォーテーションで囲っておこうぜ！ってなることもあると思います。私のところもそうでした。

なのでタイトル通り**golangでcsvの各要素をダブルクォーテーションでくくろう！**というのが、今回やりたいことになります。

しかし、golangの標準csv encoderだと、必ず"にはエスケープ文字として"が付くため、`"column"` → `"""column"""` になってしまうんですよね。さてどうしたもんか

## 結論

基本は[gocsv](https://github.com/gocarina/gocsv)を利用します。
素直に使うと[こんなencode処理](https://play.golang.org/p/jKr8EStJ2KQ)、こういう出力になります。

```
client_id,client_name,client_age
12,John,21
13,Fred,
14,James,32
15,Danny,
```

<details><summary>コード全文</summary><div>

```golang:base.go
package main

import (
	"fmt"
	"bytes"

	"github.com/gocarina/gocsv"
)

type Client struct { // Our example struct, you can use "-" to ignore a field
	Id      string `csv:"client_id"`
	Name    string `csv:"client_name"`
	Age     string `csv:"client_age"`
	NotUsed string `csv:"-"`
}

//base data
func basedata() []*Client {
	clients := []*Client{}
	clients = append(clients, &Client{Id: "12", Name: "John", Age: "21"}) // Add clients
	clients = append(clients, &Client{Id: "13", Name: "Fred"})
	clients = append(clients, &Client{Id: "14", Name: "James", Age: "32"})
	clients = append(clients, &Client{Id: "15", Name: "Danny"})
	return clients
}

func main() {
	clients := basedata()
	out := bytes.Buffer{}
	err := gocsv.Marshal(&clients, &out) // Get all clients as CSV string
	if err != nil {
		panic(err)
	}
	csvContent := out.Bytes()
	fmt.Println(string(csvContent)) // Display all clients as CSV string

}
```

</div></details>

encode前のこちらの各要素にいくら`"`をくっつけてもMarshalで`"""`に変えられてしまうので、発想を変えます。
一旦encodeしたbyte列をdecodeすると要素毎にstringで取れるので、そこに`"`をくっつけます。

gocsvでは区切り文字や改行コードの制御ができるので、そこから取ってきます。

デフォルトでは `gocsv.DefaultCSVWriter` を実行して取得できる `gocsv.SafeCSVWriter` の値なので、`,`。改行コードは`¥n`です。

こちらの情報は`gocsv.SetCSVWriter`で変更されるかもしれませんので、そこは実装に合わせて変えてください。

サンプルは[こちら](https://play.golang.org/p/WZdtIwDm-mV)、処理を抜粋。[Gist](https://gist.github.com/developer-kikikaikai/900ea4c5d2e95037b00779f8de6477fb)

```golang:base.go
//Marshalの第二引数をそのままこちらに渡す
func convert(b *bytes.Buffer) ([]byte, error) {
        //retry to decode, to check ""
        reader := csv.NewReader(b)
        reader.LazyQuotes = true
        lines, err := reader.ReadAll()
        if err != nil {
                return []byte{}, err
        }

        //rewrite to add "", escape \"
        bytes := make([]byte, 0, len(b.Bytes())*2)

        //If you update writer by SetCSVWriter, please change the delimiter which you use
        delimiter := ','
        //If you update writer by SetCSVWriter, please change the crlf which you use
        for _, line := range lines {
                for i, part := range line {
                        if i != 0 {
                                bytes = append(bytes, byte(delimiter))
                        }
                        bytes = append(bytes, []byte(escape(part))...)
                }
                bytes = append(bytes, byte('\r'))
        }
        return bytes, nil
}

func escape(part string) string {
        //"XXX" => XXX
        escapeStr := strings.Replace(part, "\"", "\"\"", -1)
        return "\"" + escapeStr + "\""
}
```

実行結果。tagはそのままにしたい場合は、convert内のlinesループを調整して最初をスキップすればOKです

```
"client_id","client_name","client_age"
"12","John","21"
"13","Fred",""
"14","James","32"
"15","Danny",""
```

<details><summary>おまけ</summary><div>
## おまけ 結論に至るまでの経緯

golangでのcsv実装に苦しんだおっさんのサタデーナイトをお楽しみください

きっかけ: 「csvのダブルクォーテーションが扱えてません！」という話があったので、調査をした

[参考記事](https://teratail.com/questions/42625)をヒントに、エンコーダーのどこかでラップしてあげるのが手なんだろうなーと思いみていく
以下泥沼調査の流れになります

- gocsv内で`SetCSVWriter`を利用して、io.Writerや `encoding/csv` の`csv.Writer`は上書きできないのだろうか？
    - io.Writerはbyte列の制御を行うので、ここに自作をかますのはもうcsvのパーサーを独自自作するのと変わらない。却下
    - encoding/csv側はinterfaceではなく内部で定義した構造体をやりとりしてるので、golangの仕様的に上書きは無理
    - →この案はダメ！
- 逆にgocsvが使っているwriterを変えて、色々処理を実行して`"`追加ができないか？
    - gocsvで`CSVWriter`というinterfaceを定義しているぞ！使えそう！
        - いや、SetCSVWriterでwriterの生成関数を登録することは可能なんだけど、関数の引数が`CSVWriter`じゃなくて`*SafeCSVWriter`。型が合わない。。。
        - ん、でもgocsvの使い方としてはSafeCSVWriterをCSVWriterのinterface以外で使ってないぞ！CSVWriterに定義変えても動くのでは？
        - [fork](https://github.com/developer-kikikaikai/gocsv/tree/abstruct_csvwriter)してCSVWriter上書き試してみよう。お、いけるやん！
        - ダブルクォーテーションのサンプルコードも詰め込んじゃお！公式取り入れてくれたら最高だよなー、まあいいや。CSVWriterも自作したものを動かして、実際のケースでテスト書くか
        - うわ、これで設定したダブルクォーテーションまでエスケープされてるやんけ！
        - ここまでは良さそうだけど、解決まではいけないらしい
    - gocsvのdecodeは"の扱いうまいし要素単位のデータが取れるぞ！
        - そしたら上でやったダブルクォーテーションのコードをcolumn単位で取り出してあげればエスケープなしの文字が取れるな！これを使って`[]byte`に書き込んで再構築すればいいんじゃないか？
        - うん、できるな。文字列中の"もエスケープが抜けてるからそれを追加しよう！
        - よし！うまく行ったぜ！一休みしたらgocsvにPR出して解決だ！
        - 、、、あれ、`CSVWriter`いじったの無意味じゃね？

という、結果無駄に終わる公式コードのデバッグと修正を行ったサタデーナイトフィーバーでした

頑張った痕跡をPR出そうか迷ったんですが、ユースケースがゼロになったので出す価値ないなと思い保留。代わりにこの記事とGistを書き留めるに至りました
</div>

## 参考
[CSVファイルの一般的書式 (RFC4180 日本語訳)](http://www.kasai.fm/wiki/rfc4180jp)
[4.1.6　CSV形式の記述規則](http://itdoc.hitachi.co.jp/manuals/3020/30203698A0/swrj0068.htm)
[golangでCSV出力時、要素をダブルクオートでくくることができない](https://teratail.com/questions/42625)
[gocsv 公式](https://github.com/gocarina/gocsv)