今回やること
今回は、csvファイルを読み込む時に
カラムの名称、数に関係なく構造体で取得できるように実装してみます。
※ Generics機能を使うので、goのバージョンは1.18以上である必要があります。
実装の流れ
最終的なゴールは、抽象的な構造体(CsvAbstract
)を作り、そこにcsvコンバート用のメソッド(ConvertCsv
)を生やします。
呼び出し側は、自分の好きな構造体をCsvAbstract
に当てはめて、ConvertCsv
をコールするだけ。
CsvAbstract
のData
プロパティを見て、先程当てはめた構造体のインスタンスが入っていれば完成。
今回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
のように、
抽象的な構造体を作り、そこからメソッドを作るのも手かなと思います。