LoginSignup
1
3

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-04-05

今回やること

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

実装の流れ

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

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

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

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

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

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


// ジェネリクス構造体
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データを渡すだけです。

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です。

{
  "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のように、
抽象的な構造体を作り、そこからメソッドを作るのも手かなと思います。

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3