Go言語でCSVを生成してS3にアップロードしたら空のCSVファイルが格納された話
はじめに
こんにちは、yuuchanです。
最近プロジェクトでバックエンドを担当しており、Go言語を使用してAPIサーバの実装を行っています。
とある機能を実現するためにCSVファイルを生成してS3にアップロードする処理を作成しましたが、その際APIサーバ内では期待通りCSVファイルが生成されたのにS3にアップロードすると空のCSVファイルが格納されてしまうという問題がありました。
今回は、修正前の問題があったコード(抜粋)を提示しつつ、原因箇所の説明とどのようにコードを修正したかを記そうかと思います。
問題があったコード(抜粋)
まず、修正前のコードを提示します。
func CreateCsvFile(rows [][]string) (*os.File, error) { // rows: CSVの元となるデータ
// ファイル名を生成(UUID)
uuidObj, err := uuid.NewUUID()
if err != nil {
return nil, errors.WithStack(err)
}
tmpPath := fmt.Sprintf("/tmp/csv/%s.csv", uuidObj.String())
// 新規ファイルを作成
f, err := os.Create(tmpPath)
if err != nil {
return nil, errors.WithStack(err)
}
// CSVデータを書き込む
w := csv.NewWriter(f)
if err := w.WriteAll(rows); err != nil {
return nil, errors.WithStack(err)
}
return f, nil
}
func (c *S3) UploadFile(bucketName string, key string, file *os.File) error { // file: CSVファイルを生成する処理でreturnしたfを渡している
// S3接続用のセッションを作成
svc, err := c.connect()
if err != nil {
return errors.WithStack(err)
}
// S3にファイルをアップロード
_, err = svc.PutObject(&s3.PutObjectInput{
Bucket: &bucketName,
Key: &key,
Body: file,
})
if err != nil {
return errors.WithStack(err)
}
return nil
}
問題の原因
上記処理でCSVファイルを生成しS3にアップロードしたところ、バケットには0Bのファイルが格納されていました。
では何が原因なのか見ていきましょう。
アップロードする前のCSVファイルは正しく生成されているかを確認
今回、アップロードするCSVファイルを一旦実行環境の/tmp/csv
下に作成しています。
そもそも、そこに作成したCSVファイルの中身にデータは入っているのでしょうか。
実行環境内の/tmp/csv
を確認したところ314.7KBのCSVファイルが作成されていました。
どうやら、実行環境内では正常にCSVファイルは作成できているようです。
ファイルポインタの値を確認
ん〜正直原因らしい原因がパッと思いつきません。
今回、最終的にS3にアップロードする際のBodyとしてファイルポインタを指定しています。
何か心当たりがあるわけではありませんが、一連の処理におけるファイルポインタ(fやfile)の値を確認してみることにします。
ファイル作成直後: 0x400019a1e0
CSVデータ書き込み直後: 0x400019a1e0
アップロード直前: 0x400019a1e0
3箇所でファイルポインタの値を確認してみましたが、全て同じアドレスを示しており特に問題無さそうに見えます。
ファイル位置を確認
コードを眺めていても解決しなさそうなので、os
パッケージやCSVファイルの生成に使用しているencoding/csv
パッケージのドキュメントや記事を確認することにします。
上記パッケージのドキュメントの他、ファイル操作という観点で様々な記事を読み進めたところ「ファイル位置」という言葉が気になりました。
どうやらos.File
ではファイル管理に必要な様々な情報を持っており、ファイル内のどの位置を見ているかを表現する「ファイル位置」という概念があるらしいです。
確証があるわけではありませんが、先程と同じ位置で「ファイル位置」を確認してみます。
position, err := f.Seek(0, io.SeekCurrent)
のように書くと、「ファイル位置」を確認できるそうです。
結果、以下のようになりました。
ファイル作成直後: 0
CSVデータ書き込み直後: 322228
アップロード直前: 322228
ファイル作成直後とそれ以外とで、「ファイル位置」が異なります。
試しに「CSVファイルを生成する処理」のreturn直前に以下のような処理を追加し、「ファイル位置」をファイル先頭に戻してみます。
_, err = f.Seek(0, 0)
すると、S3バケットに期待通り中身のあるファイルが格納されました!
どうやらS3にアップロードする際に、Bodyにファイルの終端を見ている状態のfile
を渡してしまっていることが原因だったみたいです。
修正後のコード
前節の調査結果を踏まえ、1節で提示したコードを修正しました。
「生成したCSVファイルをアップロードする処理」は特に変更ありませんが、一応本節でも再提示します。
func CreateCsvFile(rows [][]string) (*os.File, error) { // rows: CSVの元となるデータ
// ファイル名を生成(UUID)
uuidObj, err := uuid.NewUUID()
if err != nil {
return nil, errors.WithStack(err)
}
tmpPath := fmt.Sprintf("/tmp/csv/%s.csv", uuidObj.String())
// 新規ファイルを作成
f, err := os.Create(tmpPath)
if err != nil {
return nil, errors.WithStack(err)
}
// CSVデータを書き込む
w := csv.NewWriter(f)
if err := w.WriteAll(rows); err != nil {
return nil, errors.WithStack(err)
}
// ファイル位置をファイルの先頭に戻す <- ここの処理を追加
_, err = f.Seek(0, 0)
if err != nil {
return nil, errors.WithStack(err)
}
return f, nil
}
func (c *S3) UploadFile(bucketName string, key string, file *os.File) error { // file: CSVファイルを生成する処理でreturnしたfを渡している
// S3接続用のセッションを作成
svc, err := c.connect()
if err != nil {
return errors.WithStack(err)
}
// S3にファイルをアップロード
_, err = svc.PutObject(&s3.PutObjectInput{
Bucket: &bucketName,
Key: &key,
Body: file,
})
if err != nil {
return errors.WithStack(err)
}
return nil
}
おわりに
いかがでしたでしょうか。
今回のように、ファイルポインタを受け渡して後続の処理でファイルを扱う場合、「ファイル位置」に気をつけなければいけないことが分かりました。
もしかすると、CSVファイルを作成した後にそのファイル名を受け渡して後続処理で再度ファイルを開いて使用する、のように別の書き方も良かったかもしれませんが、今回の調査を経てよりos.File
の理解が深まったかなと感じています。
Go言語は使用する機会が増えてきており、日々新しい学びを得られていると感じています。
今後も、文法の理解は勿論、パッケージで提供されている機能のつくりや振る舞いを意識し、より良いコードが書けるよう頑張っていきたいと思います。