Goで小さな道具を書いていると、配布の気楽さに甘えてしまいます。単体バイナリで渡せる前提が強いので、作る側も受け取る側も身構えなくて済みます。ところが保存先としてSQLiteを選んだ瞬間、その気楽さは少しだけ薄くなります。
以前、Windows向けの小さなGo製CLIを人に渡したとき、CGOを有効にしたままビルドしていて別のPCで起動しませんでした。あのとき以来、配布前に最初に疑うのはコードよりCGO_ENABLEDです。Goのコードはきれいでも、配る瞬間だけ急に足元がOS寄りになります。
何が増えるのか
GoでSQLiteを使う入口は素直です。database/sqlの上にドライバを差すだけなので、最初はたいした違いがないように見えます。でも実際には、どのドライバを選ぶかで配布の難しさが変わります。よく使われるgithub.com/mattn/go-sqlite3はCGO前提です。modernc.org/sqliteはpure Goで動きます。
最小のサンプルで比べると差ははっきりしていました。modernc.org/sqliteはCGO_ENABLED=0のまま実行できて、できあがったバイナリは約9.4MBでした。一方でmattn/go-sqlite3はCGO_ENABLED=0でもビルド自体は通りますが、実行すると “go-sqlite3 requires cgo to work. This is a stub” で止まります。CGO_ENABLED=1で作り直すと動きますが、Linuxではlibcへの動的リンクが付きました。
package main
import (
"database/sql"
"fmt"
_ "modernc.org/sqlite"
)
func main() {
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
panic(err)
}
defer db.Close()
_, err = db.Exec(`create table t (id integer primary key, name text); insert into t(name) values ('go');`)
if err != nil {
panic(err)
}
var name string
if err := db.QueryRow(`select name from t where id = 1`).Scan(&name); err != nil {
panic(err)
}
fmt.Println(name)
}
CGO_ENABLED=0 go build ./...
CGO_ENABLED=0 go run main.go
# mattn/go-sqlite3 を使う場合はここで止まります
# panic: Binary was compiled with 'CGO_ENABLED=0', go-sqlite3 requires cgo to work. This is a stub
面倒になるのはサイズではない
ここで気になるのは、9.4MBのpure Goバイナリのほうが大きいことです。でも配布で効くのはサイズだけではありません。6.9MB前後に収まっても、動的リンクが増えた時点で確認することが増えます。誰のPCで動かすのか、どのランタイムに寄りかかるのか、将来のビルド環境をどこまで固定するのか。そういう話が後ろから付いてきます。
私はこの増え方があまり好きではありません。自分のPCのソフトウェアくらい自由に触りたいと思っているのに、配布の段になると急に外側の都合が増えるからです。単体バイナリで済むはずだった道具に、インストーラやDLLやツールチェーンの気配が戻ってきます。便利さにはだいたい請求書が付いてきますが、SQLiteを入れたときの請求書はサイズより依存関係の欄に出ます。
先に決めておくこと
もちろん、SQLiteを使うこと自体は悪くありません。検索、トランザクション、1ファイルで完結する保存形式は本当に便利です。ローカルツールにちょうどいい場面も多いです。ただ、雑に足すと後で配布が重くなります。
私が先に見るのは、SQLiteの機能が本当に必要かどうかです。設定や数件の状態保存だけなら、JSONやBoltDB系で済むこともあります。SQLiteが必要でも、拡張やネイティブ寄りの挙動が不要ならpure Goのドライバから試したほうが気が楽です。逆に、既存の知見や互換性を優先してmattn/go-sqlite3を選ぶなら、最初からCGO込みで運用すると腹をくくったほうが後でぶれません。
Goが現代のC言語っぽく見える理由の一つは、余計なものを連れてきにくいところだと思います。だから私は、SQLiteを入れるかどうかを機能の話だけで決めません。配るところまで含めて考えます。そこを先に決めておくほうが、コードを書いている時間よりずっと効きます。