Goで構造体のタグを活用してExcelのデータを読み込む
こんにちは。エンジニアの牧田です。
以前実装したアプリケーションの機能として、Excelのデータを読み込む機能があったのですが、その時に行った手法を紹介したいと思います。(実装はGo言語(v1.18)です)
今回用いているサンプルコードは↓にあります。
https://github.com/bbj14/go-tag-example/tree/v1
背景
例として以下のようなExcelの情報を読み込むことを考えます。
この各人の情報を読み取るためにPersonという構造体を作ります。
type Person struct {
Name string
Age int64
Weight float64
BirthDate time.Time
}
Excelの読み込みは、excelizeというライブラリを使って行うことにします。
excelizeではGetRowsというメソッドが用意されており、これを使うことによってシートのセル全体を[][]stringに変換することができます。
f, err := excelize.OpenFile("./sample.xlsx")
rows, err := f.GetRows("Sheet1")
あとは、rowsから各セルの値を取得すればいいわけですが、Excelの列名(A,B,C…)から値を取得するために、getColという関数を作りました。(excelize.ColumnNameToNumberは、列名を配列のインデックスに変換する関数です。)
// 行から特定の列の値を取得
func getCol(row []string, col string) (string, error) {
c, err := excelize.ColumnNameToNumber(col)
if err != nil {
return "", err
}
// 行の最後の空欄はGetRowsで読み込まれないため、そこを参照している場合は空文字を返す
if len(row) < c {
return "", nil
}
return row[c-1], nil
}
これを使うと、各行(ラベルを除く2行目以降)に対して構造体に値をセットするコードは、以下のようになると思います。(エラー処理は省略)
for _, row := range rows[1:] {
name, err := getCol(row, "A")
age, err := getCol(row, "B")
weight, err := getCol(row, "C")
birthdate, err := getCol(row, "D")
p := Person{}
p.Name = name
p.Age, err = strconv.ParseInt(age, 10, 64)
p.Weight, err = strconv.ParseFloat(weight, 64)
p.Birthdate, err = dateparse.ParseAny(birthdate)
// (pを使った処理を書く)
}
しかし、この実装には以下のような問題があります。
- Excelの列が増えていくに連れて、コードがどんどん長くなってしまうので、バグが生まれやすくなってしまう
- 構造体のフィールドの型を変えたくなった場合、値を読み込む方の実装もそれに合わせて修正しなければならない
そこで、構造体のタグを使うと、この処理をより簡潔に書くことができます。
構造体のタグについて
構造体のフィールドにメタ情報を与えるために、タグを使うことができます。
よく目にするタグとしては、以下のようなjsonタグがあると思います。
type Person struct {
Name string `json:"name"`
Age int64 `json:"age"`
Weight float64 `json:"weight"`
Birthdate time.Time `json:"birth_date"`
}
jsonタグは、例えばjson.Marshalで構造体をJSONに変換するときにJSONのキー名を指定する、というように使われます。今回は、Excelの列名を表すcolumnタグを作成します。
type Person struct {
Name string `column:"A"`
Age int64 `column:"B"`
Weight float64 `column:"C"`
BirthDate time.Time `column:"D"`
}
実装
付与したタグの情報はreflectパッケージを使って取得することができます。
各行の値が入っている[]stringを構造体の各フィールドに割り当てる関数は以下のようになります。
// 構造体のタグを元にrowの値を割り当てる
func unmarshalRows(row []string, v any) error {
var f func(reflect.Value) error
f = func(v reflect.Value) error {
for i := 0; i < v.NumField(); i++ {
structField := v.Type().Field(i)
field := v.Field(i)
// 構造体が埋め込まれていた場合、再帰的に探索
if structField.Anonymous {
f(field)
continue
}
// columnタグの値を取得
col := structField.Tag.Get("column")
if col == "" {
continue
}
str, err := getCol(row, col)
if err != nil {
return err
}
var val any
// stringをフィールドの型に合うように変換
switch field.Interface().(type) {
case string:
val = str
case int64:
val, err = strconv.ParseInt(str, 10, 64)
case float64:
val, err = strconv.ParseFloat(str, 64)
case time.Time:
val, err = dateparse.ParseAny(str)
default:
return fmt.Errorf("unexpected type: %s", field.Type())
}
if err != nil {
log.Println(err)
continue
}
// 構造体のフィールドに値をセット
field.Set(reflect.ValueOf(val))
}
return nil
}
if err := f(reflect.ValueOf(v).Elem()); err != nil {
return err
}
return nil
}
reflectパッケージを使うことによって、構造体のフィールドの数だけループを回し、各フィールドに対して
- フィールドに設定されたタグを読み取ることでExcelの列名を知る
- rowsからExcelの列名に対応したstring値を得る
- string値をフィールドの型に変換して構造体のフィールドにセットする
という操作を行っています。
このreflectパッケージを使った構造体のフィールドごとの処理は、実はjson.Unmarshalの内部でやっていることと似ています(json.Unmarshalはもっと複雑ですが)。
https://cs.opensource.google/go/go/+/refs/tags/go1.18.4:src/encoding/json/decode.go;l=96
また、再帰的な処理をするために、関数内で新たに関数fを定義しています。もし構造体のフィールドが、埋め込まれた構造体だった場合は、再帰的にfを実行します。これによって、例えば以下のような構造体も適切に値を読み取ることができます。
type Teacher struct {
Person
Class int64 `column:"E"`
Subject string `column:"F"`
}
main関数と実行結果は以下の通りです。
func main() {
f, err := excelize.OpenFile("./sample.xlsx")
if err != nil {
log.Fatal(err)
}
rows, err := f.GetRows("Sheet1")
if err != nil {
log.Fatal(err)
}
// 2行目以降に対して
for _, row := range rows[1:] {
p := Person{}
if err := unmarshalRows(row, &p); err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", p)
}
}
$ go run main.go
{Name:佐藤 Age:10 Weight:42.7 Birthdate:2011-06-05 00:00:00 +0000 UTC}
{Name:鈴木 Age:20 Weight:57.2 Birthdate:2001-09-21 00:00:00 +0000 UTC}
{Name:高橋 Age:30 Weight:68.3 Birthdate:1991-07-19 00:00:00 +0000 UTC}
{Name:田中 Age:40 Weight:58.9 Birthdate:1981-03-06 00:00:00 +0000 UTC}
{Name:伊藤 Age:50 Weight:72.5 Birthdate:1971-11-08 00:00:00 +0000 UTC}
まとめ
今回は、Goで構造体のタグを使用して、Excelのデータを読み込む方法を紹介しました。構造体のタグは、普段のGoプログラミングではあまり使うものではありませんが、適切に使うことでより簡潔な処理を書くことができるので、使える機会があるかどうか意識してみるとよさそうです。
参考にした情報
- プログラミング言語Go Alan A.A. Donovan (著), Brian W. Kernighan (著), 柴田 芳樹 (翻訳)
- https://logmi.jp/tech/articles/324582