【Golang】echoサーバーでcsvファイルをstructにする&structをcsvにする

## はじめに

Golangサーバーを新規に構築する中で、CSVファイルの入出力を汎用的にした備忘録です。
GitHub: https://github.com/Sunochi/go_echo_server

## TL;DR

SJIS形式をUTF8形式に`transform.NewReader`で変換して、`csvutil`で構造体にマッピングする。
1行ごとに処理をせずに、全行一気に処理している。
膨大な行のcsvには向かないため、もしバッチ処理などで使用する場合は今後の修正にご期待ください。
 
https://github.com/Sunochi/go_echo_server/blob/master/internal/csv/csv.go

### csv読み込み

```csv.go
func ReadFile(fh *multipart.FileHeader, s interface{}) {
	csvFile, err := fh.Open()
	if err != nil {
		log.Println("csv file open error:", err)
	}
	defer csvFile.Close()

	csv, err := io.ReadAll(csvFile)
	if err != nil {
		panic(err)
	}
	// SJIS形式 => UTF8形式
	b, err := io.ReadAll(transform.NewReader(strings.NewReader(string(csv)), japanese.ShiftJIS.NewDecoder()))
	if err != nil {
		log.Println("csv trans error:", err)
	}

	err = csvutil.Unmarshal(b, s)
	if err != nil {
		log.Println("csv file unmarshal error:", err)
	}
}
```

### csv出力

```csv.go
func WriteFile(filePrefix string, s interface{}) (string, string, error) {
	b, err := csvutil.Marshal(s)
	if err != nil {
		log.Println("struct to csv error:", err)
		return "", "", err
	}
	// UTF8 => SJIS形式
	sjisByte, _, err := transform.Bytes(japanese.ShiftJIS.NewEncoder(), b)
	if err != nil {
		log.Println("csv trans error:", err)
		return "", "", err
	}

	fileName := filePrefix + "_" + time.Now().Format("20060102150405.csv")
	filePath := "csv/" + fileName
	err = os.WriteFile(filePath, sjisByte, 0666)
	if err != nil {
		log.Println("file write error:", err)
		return "", "", err
	}
	return filePath, fileName, nil
}
```

## 解説

### 読み込み

```csv.go
func ReadFile(fh *multipart.FileHeader, s interface{}) {
	csvFile, err := fh.Open()
```

echoではアップロードされたファイルを
`fh, err := c.FormFile("user_csv")` のようにして受け取ることができる。 - [code](https://github.com/Sunochi/go_echo_server/blob/master/api/user.go#L18)
それを第一引数に設定し、第二引数に**構造体を参照渡し**する。
参照渡しする理由は後ほど使用する `csvutil` に入れるため。

```csv.go
    csvFile, err := fh.Open()
	if err != nil {
		log.Println("csv file open error:", err)
	}
	defer csvFile.Close()
```
ファイルを開き、関数の処理が完了したときにファイルを閉じるために`defer`を設置する。

```csv.go
    // SJIS形式 => UTF8形式
	b, err := io.ReadAll(transform.NewReader(strings.NewReader(string(csv)), japanese.ShiftJIS.NewDecoder()))
```
先ほど読み込んだバイナリ（io.Reader）はSJIS形式のため、transform.NewReaderを通してUTF8形式に変換する。
※CSVファイルのエンコード形式をSJISに固定して処理しているため、SJIS以外が入ると文字化けする。

```csv.go
    err = csvutil.Unmarshal(b, s)
```
第二引数に参照渡しした構造体にマッピングする。
構造体には `csv:"ヘッダー名"` でタグづけが必須。
ex. 
```
type User struct {
	ID      uint   `csv:"id"`
	Name    string `csv:"name"`
	Phone   string `csv:"phone"`
	Address string `csv:"address"`
}
```

### 出力

読み込みの逆手順を行う。
関数の呼び出し元で `defer os.Remove(第二戻り値のファイルパス)`
を実行すると、csvファイルが自動で削除される。
出力先の指定を柔軟にできないため、必要であれば
```csv.go
filePath := "csv/" + fileName
```
ここの "csv/" を環境変数や引数で指定できるようにするといい。

## もし修正するなら

### メモリ周り

元々マスタデータ周りで使用するために作ったため膨大な量のデータを想定していない。
しかし、メモリ効率が最悪な実装なのでgoroutine を使用して、

`IO.Read` で1行ごとにマッピング処理 
-> 一定行変換する or 読み込み完了
-> DBにbulk upsertする
のような処理順にした方がいいと思う。

### 文字エンコーディング周り

要件次第では、SJIS以外でも入力できるよう自動で判定する処理を実装するべき。

#### ポインタ強制

`Golang 1.18` から出たジェネリクスを使用すれば、第二引数の `s interface{]` のポインタを強制できる。


## 最後に

**もし修正するなら** に書いたgoroutine での実装ができたら、また記事にしようと思います。
要件に対する最小限の実装（YAGNI）が個人的な流行りで、将来的なケースをどこまで担保するかの線引きが難しいです。