【golang】ジェネリクスを使ったcsvファイル読み込み

# 今回やること
今回は、csvファイルを読み込む時に
カラムの名称、数に関係なく構造体で取得できるように実装してみます。
※ Generics機能を使うので、goのバージョンは1.18以上である必要があります。

# 実装の流れ
最終的なゴールは、抽象的な構造体（`CsvAbstract`）を作り、そこにcsvコンバート用のメソッド(`ConvertCsv`)を生やします。
呼び出し側は、自分の好きな構造体を`CsvAbstract`に当てはめて、`ConvertCsv`をコールするだけ。
`CsvAbstract`の`Data`プロパティを見て、先程当てはめた構造体のインスタンスが入っていれば完成。

今回csv関連で使用するパッケージは、`github.com/jszwec/csvutil`です。

# 抽象化した構造体とジェネリクス関数を定義

ジェネリクス機能を使うことで、「型が異なるだけで同じ処理をもつ複数の関数」を「1つの関数」として定義することができます。
ジェネリクスを使用しない場合、csv読み込み処理を型の数分作ることになります。

まずは、構造体を抽象化した`CsvAbstract`を定義します。
ジェネリクス対応の構造体を定義するには、構造体名の後ろにブラケット ([]) で囲んだ 型パラメーター (type parameters) を記述し、その型を構造体内で使用します。これは、ジェネリクス対応でも同じです。

次に、ジェネリクス関数（`ConvertCsv`）を作成します。
CsvAbstractインスタンス内の値を変更するので、ポインタレシーバにしましょう。

```golang

// ジェネリクス構造体
type CsvAbstract[T any] struct {
	Data []T
}

func (t *CsvAbstract[T]) ConvertCsv(file multipart.File, fileHeader *multipart.FileHeader) {
	// *csv.Readerに変換
	reader := csv.NewReader(file)
	reader.LazyQuotes = false // ダブルクオートを厳密にチェックする。この辺りはお好きなように。

    // *csvutil.Decoderに変換
	dec, err := csvutil.NewDecoder(reader)

	if err != nil {
		fmt.Println(err)
	}

	for {
		var data T

		// 1行ずつデコード
		if err := dec.Decode(&data); err == io.EOF {
			fmt.Println("Completed read csv data.")
			break
		} else if err != nil { // 以降、丁寧に書いていますが、この辺りもお好きなように。
			// パースエラーが起きた場合
			if e, ok := err.(*csv.ParseError); ok {
				n := "データ形式に誤りがあります。"
				switch e.Err {
				case csv.ErrBareQuote:
					// ダブルクオート途中で使用されていて LazyQuotes を true にしていない場合のエラー
					// 例えば、 Yam"ada,test@test.com,29 のように 途中に " がある場合
					n = "データの途中に\"（ダブルクオーテーション）が含まれている可能性があります。"
				case csv.ErrQuote:
					// 先頭がダブルクオートで始まっていて、末尾がダブルクオートになっていない場合のエラー
					// 例えば、 "Yamada,test@test.com,29 のように閉じるための " がない場合
					n = "\"（ダブルクオーテーション）が不足している可能性があります。"
				case csv.ErrFieldCount:
					n = "指定されたカラム数と実際のデータが合っていません。"
				}
				fmt.Println("\nエラー内容: ", n, "\n", e.Err,
					"\nStartLine:", e.StartLine, "\nLine:", e.Line, "\nColumn:", e.Column)
			}
		}
        // 先程定義した構造体にデータを入れます。この時点で、インスタンスになっています。
		t.Data = append(t.Data, data)
	}
	return nil
}

```


# 実際に呼び出してみる
まず、`CsvUser`という構造体を作ります。
`CsvUser`のプロパティは、csvファイルのカラム名と対応するようにします。
csvのカラム名と、構造体のプロパティ名を合致させるために、エイリアス（`csv:"xxx"`）を設定しましょう。
これがないと、正しく読み込まれません。
今回の例だと、8列のcsvファイルを想定して作ってみました。

次に、実際に先程のジェネリクスを呼び出すための`CallGenerics`関数を定義します。
この中で、ジェネリクス構造体を初期化します。
この際、型パラメーター（`[]`部分）に`CsvUser`を定義しています。
これによって、`CsvUser`用の`CsvAbstract`構造体になりました。
あとは、`ConvertCsv`関数に、csvデータを渡すだけです。


```golang
type CsvUser struct {
	UserName       string `csv:"user_name"`
	MailAddress    string `csv:"mail_address"`
	PostalCode     string `csv:"postal_code"`
	Prefecture     string `csv:"prefecture"`
	City           string `csv:"city"`
	Block          string `csv:"block"`
	Building       string `csv:"building"`
	PhoneNumber    string `csv:"phone_number"`
}

func CallGenerics(file multipart.File, fileHeader *multipart.FileHeader) {
	csvUsers := []CsvUser{}

    // ジェネリクス構造体を初期化
	csvAbstract := CsvAbstract[CsvUser]{
		Data: csvUsers,
	}

    // コンバートメソッドを実行
	csvAbstract.ConvertCsv(file, fileHeader)

    // json化する
	out, _ := json.Marshal(csvAbstract)
	fmt.Println(string(out))
	return
}
```


データ確認用に、jsonでコンソールに出力しています。
こんな感じで、データが出力されていればOKです。

```json
{
  "Data": [
    {
      "UserName": "田中太郎",
      "MailAddress": "test@test.com",
      "PostalCode": "1234567",
      "Prefecture": "テスト県",
      "City": "テスト市",
      "Block": "テスト区1-1-1",
      "Building": "テストビル4F",
      "PhoneNumber": "09012345678"
    },
    {
      "UserName": "田中次郎",
      "MailAddress": "test2@test.com",
      "PostalCode": "1234567",
      "Prefecture": "テスト県",
      "City": "テスト市",
      "Block": "テスト区1-1-1",
      "Building": "テストビル4F",
      "PhoneNumber": "08098765432"
    }
  ]
}
```

# まとめ
いかがでしたでしょうか。
型があると、共通処理の作成で苦労することが多々あると思います。
そんな時は、今回の`CsvAbstract`のように、
抽象的な構造体を作り、そこからメソッドを作るのも手かなと思います。