Go 1.22 から実験的機能として Range Over Func が実装された。 このブログでは Range Over Func とは何か、どういうものなのか、といった説明はしないので、知らない人は次のページを見てほしい。
標準パッケージの database/sql で RDB を Query するのはボイラープレートまみれになる。
database/sql のドキュメントにあるサンプルコードは次のようになっている。
rows, err := db.QueryContext(ctx, "SELECT name FROM users WHERE age=?", age) if err != nil { log.Fatal(err) } defer rows.Close() names := make([]string, 0) for rows.Next() { var name string if err := rows.Scan(&name); err != nil { log.Fatal(err) } names = append(names, name) } // Check for errors from iterating over rows. if err := rows.Err(); err != nil { log.Fatal(err) }
https://pkg.go.dev/database/sql#example-Rows から引用。
このコードは長い。 error チェックを3回もしているのが大きな要因だ。さすがに SELECT 文の実行という定型的なことに毎回こんなコードを書きたくない。面倒だ。
だから、このサンプルコードのような (*DB).QueryContext
と (*Rows).Next
と (*Rows).Scan
の一連の流れを、関数として独立して、いかなるクエリでもボイラープレートを書かずに済ましたい。以下このような関数の名前を QueryScan
にする。
よし、 QueryScan
を書こう!
func QueryScan(ctx context.Context, db *sql.DB, query string, args ...any) ... { ...??? }
…って、あれ?この関数の実装ってそんな簡単じゃない?
そう、自明ではない問題がいくつかある。
もっとも大きな問題は (*Rows).Scan
の引数がクエリごとに違うこと。型も引数の数もクエリごとに違う。これらの情報をどうやって渡すの?そして返り値の型はどうするの?
reflection つかうの?あれ面倒くさいなこれ。となる。
普通の開発ではサードパーティの SQL ライブラリを使うのがいいんだろうけど、今回それは置いておこう。また reflect パッケージで頑張ってつくるのも今回はしない。QueryScan
関数は画面に収まる程度にしたい。
このブログではそれらの問題を Range Over Func を使うことで、まあまあ解決した関数 QueryScan
を提案する。それは (*DB).QueryContext
と (*Rows).Next
の一連の流れを、関数としてまとめたものだ。
Range Over Func を使った実装
提案する関数は次のように定義される。
type scanfunc func(dest ...any) func QueryScan(ctx context.Context, db *sql.DB, query string, args ...any) iter.Seq2[scanfunc, error]
この関数の実装は後で紹介する。
そして QueryScan
関数を使うと、このブログで最初に提示したサンプルコードは次のようになる。
names := make([]string, 0) for scan, err := range QueryScan(ctx, db, `SELECT name FROM users WHERE age=?`, age) { if err != nil { log.Fatal(err) } var name string scan(&name) // scan には返り値がない。返り値を無視しているわけではない。 names = append(names, name) }
うおおおおおお
error のチェックが3回から1回に減って、コード行数も16 行から 9行に減っている。 コードが短い分読みやすいのではないだろうか。
この関数のポイントとしては (*Rows).Scan
自体は結局手で書かないといけないところだ。エラーの返り値がないラップされた関数 scan
を使っているので多少は楽になっているものの、この部分の面倒さは残る。しかしながら、 (*Rows).Scan
の引数がクエリごとに違うという問題は解決している。
では QueryScan
はどのような実装になっているのだろうか。中身を見てみよう。
type scanfunc func(dest ...any) // 再掲 func QueryScan(ctx context.Context, db *sql.DB, query string, args ...any) iter.Seq2[scanfunc, error] { return func(yield func(scanfunc, error) bool) { rows, err := db.QueryContext(ctx, query, args...) if err != nil { yield(nil, err) return } defer rows.Close() for rows.Next() { var scanErr error if !yield(func(dest ...any) { scanErr = rows.Scan(dest...) }, nil) { return // ここは困る!scanErr が渡せないからだ。後述する。 } if scanErr != nil { yield(nil, scanErr) return } } if err := rows.Err(); err != nil { yield(nil, err) return } } }
QueryScan
の実装は、最初に引用した pkg.go.dev にあるサンプルコードと似ていることに気がつくだろうか。むしろほとんど一緒である。違うのは (*Rows).Scan
(をラップした関数)と error を yield
に渡しているところだ。
yield
関数が呼ばれるたび、 range over func の for 文の中身が1回実行される。言い換えると、for のループが1回まわる。QueryScan
の db.QueryContext
とそのエラー処理をしているところに注目してみよう。つぎの部分だ。
rows, err := db.QueryContext(ctx, query, args...) if err != nil { yield(nil, err) return }
yield
が1回だけ実行されるので、 QueryScan
のユーザー(呼び出し側)から見ると、ループが1回だけまわって終了しているように見える。このループで渡される値は、(nil, err)
だ。つまりエラーだけを渡している。
yield
を呼び出している別のところも見てみよう。たとえば、 rows.Scan
が失敗するとどうなるのか?
あるループの中で、 scan が失敗すると、次にもう1回だけ scanErr を渡すためのループが実行される。yield(nil, scanErr)
の部分だ。そしてループは終了する。
この QueryScan
の問題点
提案しておいてなんだが、この QueryScan
は面白いものの、Range Over Func のハックだなぁとは思う。オーバーエンジニアリングかもしれない。また問題点もある。
大きな問題点は、for で error が渡されたときだけ break すると仮定していることだ。 error が渡されたループで break するのは問題はない。(というか error が渡されたときは最後のループなので、 break してもしなくても同じだ。)しかしながら、正常系つまり nil error が渡された時に break されると困ることがある。なぜなら次のループで、 error が渡されるかもしれないが、それを無視することになるからだ。 前述した QueryScan
の実装で「ここは困る!」とコメントした部分だ。
for scan, err := range QueryScan(...) { if err != nil { log.Print(err) break // ここの break は問題がない。 return でもよい。 } ... // もしかすると、次のループで error が渡ってくるかもしれない。 // ここで break するとその error チェックができない。 break }
このような break
をするケースはほとんどないだろうけど。
一応解決策はある。それは break ができてしまう range over func をやめること。この記事の前提を崩すように見えるが、そんなこともない。QueryScan
の返り値を iter.Seq2
すなわち
func(yield func(scanfunc, error) bool)
ではなく、
func(yield func(scanfunc)) error
で書くということだ。 Range Over Func は yield
の返り値の bool で break かどうか分岐していた。 yield
の返り値がなくなったので、 break できなくなった。また error をかえす場所を変えた。後者をかえす QueryScan
の実装は宿題とする。
for 文では書けなくなるが、考え方自体は変わっていない。たとえば次のように呼び出すことになる。
err := QueryScan(ctx, db, `SELECT name FROM users WHERE age=?`, age)(func(scan scanfunc) { var name string scan(&name) names = append(names, name) })
こっちの方が好きな人がいるかもしれない。こちらはbreak の問題がなくなり、エラーの渡し方も標準的だ。僕もこっちの方が好きかもしれない。
まあこのブログは新機能の range over func をせっかくなら使ってみたという欲張りがあったので、最初は range over func を使った手法を紹介したのだった。