0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

はじめに

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

TL;DR

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

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
それを第一引数に設定し、第二引数に構造体を参照渡しする。
参照渡しする理由は後ほど使用する 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)が個人的な流行りで、将来的なケースをどこまで担保するかの線引きが難しいです。

0
1
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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?