Journal

SQLiteを入れた瞬間に、Goの配布は少しだけ面倒になる

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を入れるかどうかを機能の話だけで決めません。配るところまで含めて考えます。そこを先に決めておくほうが、コードを書いている時間よりずっと効きます。

Previous