🎉

goでcsvを扱うパッケージを新しく作った理由、改善点、3つの新機能

2023/09/08に公開

xsv

今回作成したパッケージは以下にあります。
https://github.com/shigetaichi/xsv

xsvは、golangでcsvの読み取り・書き込み処理のための便利な処理を提供するものです。

  • gocsvにある問題点を解消した上で、
  • 頻繁に使われるであろう機能を追加し、
  • パフォーマンスも維持している

といったパッケージです。

大部分を https://github.com/gocarina/gocsv/ を基に作成しました。独自で公開するパッケージとしては初めてのものです。

同じ問題に遭遇した人に「便利だなあ」と思ってもらえるようなものになると嬉しいです。

使い方(書き込み)

使い方は非常に簡単です!gocsvを知っている方なら尚更簡単です。
読み取り処理もほぼ同様なので、ぜひREADME.mdのUsageを見てみてください🙇

👇 こんな感じで構造体を用意し、csvタグでヘッダーに表示する名前を指定して、書き込みたいデータを準備します。

type Client struct {
	ID            string `csv:"client_id"`
	Name          string `csv:"client_name"`
	Age           string `csv:"client_age"`
	NotUsedString string `csv:"-"`
}

clients := []*Client{
	{ID: "12", Name: "John", Age: "21"},
	{ID: "13", Name: "Fred"},
	{ID: "14", Name: "James", Age: "32"},
	{ID: "15", Name: "Danny"},
}

👇 NewXsvWrite関数で新しくxsvWrite変数を作成して、

xsvWrite := xsv.NewXsvWrite[*Client]()

👇 適宜設定を変更します。他にも色々設定項目あります👀

xsvWrite.OmitHeaders = true

👇 どこに書き込みするかを決めて(今回はファイル)、writerを設定します

clientsFile, _ := os.OpenFile("clients.csv", ...)

// SetFileWriterを使えば、Fileを渡すだけで簡単にcsv.Writerを設定できる。
xsvWrite.SetFileWriter(clientsFile)

👇 書き込みたいデータを渡して、実行(Write)!!

err = xsvWrite.SetFileWriter(clientsFile).Write(clients)
if err != nil {
	return
}
// clients.csvが生成される

開発の動機

goでcsvを扱うならgocsv→もっと便利にできる💪

golangでcsvを扱うパッケージとして、gocsvが有名です。
https://github.com/gocarina/gocsv

このパッケージは、goの構造体からcsvを容易に生成することができます。csvを読み取ってgoの構造体データとして取得することもできます。
ただ、gocsvには「グローバル変数」で設定項目を管理しているという致命的な問題点がありました。

さらに、

  • あまり積極的に開発やメンテナンスがされているわけではなさそう
  • tagによるバージョニングリリースがなかった
  • 新しく追加したい機能があった

上記のような理由と、自分でパッケージを作ってみたい欲があったので、作成を決意しました。

gocsvとxsvを比べた時に、「xsvの方が優れている、または同等」の状態にすると目標を立てました。

ネーミング

「csvだけじゃなくtsvも扱えるよ」って意味と、開発を始めた時にちょうどTwitterがXに名前を変えたので、xsvにしました。個人的には気に入っています。

改善点

gocsvの致命的問題:設定項目がグローバル変数で管理されている

issueでも上がっていましたし、僕自身もこの問題に遭遇しました。
https://github.com/gocarina/gocsv/issues/247

設定項目がグローバルで管理されていると、異なる設定項目で同時に処理を行うことができません。

csvの読み取り・書き込みにはある程度時間がかかる場合が多いので、一つの処理が行われている間に別のcsvを処理するリクエストが来ることも多いでしょう。

その場合に対応できないということです。

問題のグローバル変数群はここ👇に書かれています。
https://github.com/gocarina/gocsv/blob/99d496ca653d493041370f2e18d72bc7b1d60d7f/csv.go#L21-L115

xsvはどう解決しているか

もちろん、致命的問題である「設定項目としてのグローバル変数」はxsvに存在しません。

全ての設定項目をXsvWrite/XsvReadというストラクトのフィールドで管理するようにしています。

さらに、csv書き込み・読み込みを実行するWrite()関数とReadTo()関数は、どちらもXsvWriter/XsvReaderのメンバー関数として実装しています。
これにより、ストラクトで設定した項目で、実行されることが担保されます。

設定項目を構造体のフィールドで管理することで、柔軟にプログラムを作ることができます。
何かしらの条件でcsv出力形式を変更したいときも、以下のように記述することができます。

xsvWrite1 := NewXsvWrite()
xsvWrite2 := NewXsvWrite()
xsvWrite2.SortOrder = []int{2, 1, 0}
if condition {
	xsvWrite1.Write(data)
} else {
	xsvWrite2.Write(data)
}

新しく追加した機能

書き込みの方で、3つの機能を新しく追加しました。

  • SelectedColumns
  • SortOrder
  • HeaderModifier

それぞれの簡単な説明をしていこうと思います。

SelectedColumns

SelectedColumnsを使えば、指定したカラムだけを出力するようにできます。

xsvWrite.SelectedColumns = []string{"client_name", "client_age"}

このように設定すると、client_nameとclient_ageのみを出力します。
ストラクトのタグで指定したヘッダー名を指定することができます。
※のちに紹介するHeaderModifierで書き換える前のヘッダー名を指定してください。(ストラクトのタグに書かれているヘッダー名を参照しているので)

gocsvのissueにも同様の要望が出されていました。
https://github.com/gocarina/gocsv/issues/135
https://github.com/gocarina/gocsv/issues/198

SortOrder

csv出力時に、カラムの並べ替えを行うことができる機能です。
カラムの順番をインデックス番号を用いて指定します。

xsvWrite.SortOrder = [1, 0, 2]

このように設定すれば、出力結果は以下のようになります。

client_name, client_id, client_age
// 本来はclient_id, client_name, client_age

type Client struct {
	ID            string `csv:"client_id"` // index 0 に相当
	Name          string `csv:"client_name"`// index 1 に相当
	Age           string `csv:"client_age"`// index 2 に相当
	NotUsedString string `csv:"-"`// SortOrderの時に無視。
}

何も指定しない場合だと、出力するデータの構造体の上から順番に、csvカラムを生成します。
また、csv:"-"が指定されている場合は出力が無視されるので、SortOrderのインデックス配列の考慮に含めません。

xsvWrite.SortOrder = [1, 0, 2, 3]このように設定するとエラーが発生するようになっています。SortOrderの配列の長さは、出力されるcsvのカラム数と必ず等しくなければなりません。

昔自分でissueを作って、gocsvに出したことがあります。
https://github.com/gocarina/gocsv/issues/225

HeaderModifier

出力時に、ヘッダー情報を上書きすることができる機能です。

HeaderModifierはmap[string]string型です。
key部分に書き換え前のヘッダー、value部分に書き換え後の文字列を指定します。

xsvWrite.HeaderModifier = map[string]string{"cliend_id": "cliend_id_x"}
// or
xsvWrite.HeaderModifier["cliend_id"] = "client_id_x"

上記のように設定すると、最終出力時に、

client_id_x, client_name, client_age

となって出力されます。

既存のgocsvでは、ヘッダー情報はストラクトタグで指定したまま書き換える方法がありませんでした。出力後のcsvヘッダーを丸々置き換えるようなアプローチを取る他なかったので、xsvにはヘッダー書き換え機能を追加しました。

gocsvのissueでも同様のものがありました。
https://github.com/gocarina/gocsv/issues/128

感想・今後

既存のパッケージの問題点を見つけ、改善できたことはとても良かったです。
コードリーディングはとても億劫でしたが、わかってさえ見れば意外と簡単に思えました。
より便利なプログラムにしたい、作りたい意欲が湧いてきました。
gocsvのissueをみてみると意外と新規機能のアイデアがあるってことも気づけました。

今後も、メンテナンスは続けていくつもりです。
また、新規機能の追加・リファクタリングもしていきたいです。
githubで改善のコメント等あれば是非お願いいたします🙇

スクラップで開発のメモ書きしてます。
https://zenn.dev/taichi_sigma2/scraps/c1d6253c951e39

Discussion