Open9

ミニマリストのためのgo

podhmopodhmo

標準ライブラリだけから始めて痛くなるポイントとかユースケースをまとめておくと便利なのでは?

例えば

  • flag
  • net/http
  • database/sql

あたりを対象に慣れた人ならみんな記事をかける筈(書けない人は知識が足りない)

podhmopodhmo

なんか自分で書かなくても良くない?みたいな気持ちになったりもした。でも細かな部分での試行錯誤と疎通が通ってないとビミョー感はある。

podhmopodhmo

あと候補になりそうなのは

  • testing (go-cmpで良くね?本当?, golden fileを用意したテストの書き方とか, testify必要/不要)
  • sync (WaitGroup -> errorgroupみたいな感じのものも含める, singleflightとか)
  • (go/ast (go/packages, go vetのanalyzer, これだけでも不足な場合...))
  • (errors, html/template)
podhmopodhmo

gemini 3

今日gemini 3が使えるようになったので使ってみることにする。

go1.25+でミニマリストが標準ライブラリのみの利用から外部ライブラリを使いたくなる時やユースケースを明らかにするミニマリストのためのxxxの限界というテーマで記事を書こうと思います。

ぱっと見の感想を雑にまとめると(叩き台としては)悪くない気がする。

podhmopodhmo

net/http

ユーザー:
go1.25+でミニマリストが標準ライブラリのみの利用から外部ライブラリを使いたくなる時やユースケースを明らかにするミニマリストのためのxxxの限界というテーマで記事を書こうと思います。

ミニマリストなので当然ボトムアップでユースケースを膨らませていき対応が厳しくなるタイミングでそれへの対応を明らかにします。

簡単な箇条書きでプロットを考えてください。

対象 net/httpを使ったhttp server


AI:
Go 1.25+(Go 1.22以降の強化されたServeMuxやslogを前提とした環境)において、標準ライブラリ至上主義のミニマリストが「どこで限界を感じ、どの外部ライブラリを導入すべきか」をボトムアップで検証する記事のプロット案です。

タイトル案:
『net/httpの限界:ミニマリストが外部ライブラリに手を伸ばす時 (Go 1.25+)』


1. イントロダクション:ミニマリストの誓い

  • 現状の確認(Go 1.25+の世界観)
    • net/http.ServeMux でメソッド指定(GET /path)やパスパラメータ(/items/{id})が扱えるようになった。
    • log/slog で構造化ログも標準で完結する。
    • 「もう外部ルーター(chi, gorilla/mux)すら要らないのでは?」という仮説からスタート。

2. フェーズ1:標準ライブラリで十分な領域(幸福な時間)

  • ユースケースA:単純なREST API
    • 実装:mux.HandleFunc("GET /users/{id}", handler)
    • 結果:全く問題なし。依存ゼロで快適。
  • ユースケースB:JSONのレスポンス
    • 実装:json.NewEncoder(w).Encode(data)
    • 結果:構造体タグさえ書けば快適。

3. フェーズ2:雲行きが怪しくなる領域(ボイラープレートの増加)

  • ユースケースC:リクエストボディのJSONデコード
    • 実装:json.NewDecoder(r.Body).Decode(&req)
    • 摩擦ポイント: エラーハンドリングが冗長。「JSONが壊れているのか」「フィールドの型が違うのか」の判別ロジックを書くのが面倒になり始める。
  • ユースケースD:ミドルウェアの連鎖
    • 実装:ログ、リカバリ、CORS、認証を自前でラップする。
    • 摩擦ポイント: handler(middleware1(middleware2(...))) のようなネスト地獄、あるいは自前の Chain 関数のメンテコスト。
    • 限界の予感: まだ耐えられるが、コードの可読性が落ちてくる。

4. フェーズ3:限界点(外部ライブラリ導入の決断)

ここからが本題。「標準ライブラリだけで頑張る=逆にコードが複雑になる」タイミング。

  • 限界1:バリデーション(入力値検証)

    • 状況:必須チェック、文字列長、メールアドレス形式、数値の範囲チェックが必要。
    • 標準での対応:if len(req.Name) == 0 { ... } を延々と書く。
    • 結論: ロジックが汚れる。宣言的に書きたい。
    • 導入候補: go-playground/validator (または ozzo-validation)
      • 理由:構造体タグで宣言的に書かないと、ビジネスロジックがif文に埋もれるため。
  • 限界2:OpenAPI (Swagger) ドキュメントの乖離

    • 状況:フロントエンドチームへのAPI仕様書提供。
    • 標準での対応:コードとは別にYAMLファイルを手書きで管理。
    • 結論: 実装とドキュメントが必ずズレる。保守不可能。
    • 導入候補:
      • アプローチA(コードファースト):swag (コメントから生成)
      • アプローチB(スキーマファースト):oapi-codegen (YAMLからコード生成)
      • アプローチC(コード=仕様):Huma (Go 1.22+に特化した軽量フレームワーク)
      • 理由:ドキュメントの自動化は標準ライブラリでは逆立ちしても無理なため。
  • 限界3:リクエストデータのバインディング(型安全性)

    • 状況:パスパラメータ、クエリパラメータ、JSONボディを一度に構造体に詰め込みたい。
    • 標準での対応:r.PathValuer.URL.Queryjson.Decode を個別に呼び出して詰める作業。
    • 結論: 生産性が低すぎる。
    • 導入候補: EchoGin などの薄いフレームワーク、あるいは Huma
      • 理由:バインディングの一括処理は reflect を多用するため、自前実装はバグの温床になるため。

5. 結論:現代のGoミニマリストの戦略

  • 新しいミニマリズムの定義
    • 「依存ライブラリゼロ」ではなく「ボイラープレートゼロ」を目指す。
  • 最終的なスタック提案
    • 基本:net/http + ServeMux
    • 必須アドオン:validator (バリデーション)
    • 開発規模に応じて:oapi-codegen (スキーマ駆動) または Huma (現代的アプローチ)
  • まとめ
    • ルーティングは標準でいい。
    • しかし、「バリデーション」と「ドキュメント化」で標準ライブラリに固執すると、逆に保守コストという負債を抱えることになる。

(返答終了理由: STOP)

podhmopodhmo

flag

ユーザー:
それでは今度はアプリの作成でflagについてを対象にして同様のことを考えてください。


AI:
Go 1.25+環境において、CLIツール作成の標準ライブラリ flag パッケージを対象に、ミニマリストが「どこで限界を感じ、どの外部ライブラリへ移行すべきか」をボトムアップで検証する記事のプロット案です。

タイトル案:
『flagパッケージの限界:ミニマリストがCLIフレームワークに手を伸ばす時』


1. イントロダクション:Unix哲学への回帰

  • 現状の確認
    • GoでCLIを作るなら flag が標準。
    • 単一バイナリ、依存ゼロは運用ツールとして最強の構成。
    • 「Cobraのような重厚なフレームワークは本当に必要なのか?」からスタート。

2. フェーズ1:標準ライブラリで十分な領域(幸福な時間)

  • ユースケースA:単機能のスクリプト
    • 実装:timeout := flag.Int("t", 30, "timeout seconds")
    • 結果:非常にシンプル。go run main.go -t 60 で完結。
  • ユースケースB:独自の型変換
    • 実装:IPアドレスリストなど、flag.Value インターフェースを実装してカスタム型を受け取る。
    • 結果:少しコード量は増えるが、標準機能の範囲内で型安全に扱えるため満足度は高い。

3. フェーズ2:雲行きが怪しくなる領域(ボイラープレートの増加)

  • ユースケースC:サブコマンドの実装
    • 実装:git commitgit push のようなコマンド体系。
    • 標準での対応:flag.NewFlagSet を定義し、os.Args[1]switch 文で分岐させて、それぞれの FlagSet.Parse() を呼ぶ。
    • 摩擦ポイント: サブコマンドが増えるたびに分岐ロジックとヘルプメッセージの構築(Usage の上書き)が手動になり、main関数が肥大化する。
  • ユースケースD:位置引数(Positional Arguments)のバリデーション
    • 実装:cp <src> <dst> のようなフラグではない引数。
    • 標準での対応:flag.Parse() 後の flag.Args() の長さをチェック。
    • 摩擦ポイント: 「引数が足りない場合のヘルプ表示」を自前で fmt.Fprintf(os.Stderr, ...) する必要があり、統一感がなくなる。

4. フェーズ3:限界点(外部ライブラリ導入の決断)

ここからが本題。「標準の flag はGo独自の作法であり、現代的なCLIの期待値に応えるには辛くなる」タイミング。

  • 限界1:ショートハンドとロングフラグの共存 (POSIX準拠)

    • 状況:ユーザーは -v--verbose の両方が使えることを期待する。また、-abc-a -b -c と解釈されることを期待する。
    • 標準での対応:flag パッケージはGo独自のスタイル(-verbose シングルハイフン)のみ推奨。エイリアスを作るには変数を共有したフラグを2つ定義する必要がある。
    • 結論: 定義が冗長になりすぎる。Unix/Linux標準の挙動を再現できない。
    • 導入候補: spf13/pflag
      • 理由:標準の flag とほぼ同じAPIで、POSIX準拠(GNUスタイル)のフラグ処理が可能になるため。
  • 限界2:環境変数からの設定読み込み (12-Factor App)

    • 状況:コンテナ環境で動かすため、フラグ指定がない場合は環境変数 APP_PORT を読み込んでほしい。
    • 標準での対応:os.Getenv で値を取り、フラグのデフォルト値に突っ込むか、優先順位を判定するロジックを全フラグに書く。
    • 結論: 設定の優先順位(フラグ > 環境変数 > デフォルト値)の管理が苦行。
    • 導入候補: peterbourgon/ff
      • 理由:標準の flag.FlagSet を拡張する形で、環境変数や設定ファイルからの読み込みを透過的に追加できるため(ミニマリスト御用達)。
  • 限界3:構造体へのマッピングと階層構造

    • 状況:設定項目が50を超え、サブコマンドがネストする(例:app get config users)。フラグ変数をグローバル変数や個別のローカル変数で管理するのが限界。
    • 標準での対応:変数の管理だけでコードがスパゲッティ化する。
    • 結論: 変数定義ではなく、構造体タグで宣言的に定義したい。
    • 導入候補: alecthomas/kong (または spf13/cobra)
      • 理由(Kong):構造体タグを使って宣言的にCLIを定義でき、バリデーションやヘルプ生成が自動化される。CobraよりGoらしく型安全でミニマルに書ける。
      • 理由(Cobra):業界標準が必要な場合。

5. 結論:現代のGoミニマリストの戦略 (CLI編)

  • 基準の再定義
    • 「自分だけが使うツール」か「他人に配布するツール」かで境界線を引く。
  • 最終的なスタック提案
    • 自分用スクリプト/単機能:標準 flag
    • コンテナ/クラウド対応ツール:標準 flag + ff (環境変数対応のため)
    • 配布用CLI/複雑なツール:kong (宣言的記述のため) ※Cobraはボイラープレート生成前提なのでミニマリスト的には次点
  • まとめ
    • flag は優秀だが、ユーザー体験(UX)と設定の柔軟性(環境変数)を求め始めた瞬間、外部ライブラリを使ったほうがコード記述量は減り、結果的にミニマルになる。

(返答終了理由: STOP)


ユーザー:
やるじゃんでもpflagの導入はflagのtextvar対応で遅らせられるようになった。そして-2つも実は対応してる。そう考えると、short flagと(あと実は有名ではないけど実は重要なのは)SetInterspersedを利用したくなる時。この対応は不足してます。

環境変数対応もWalkで対応できますがこれをヘルプメッセージに載せたい時に苦労します(cobraもそれほど楽ではありませんが)。そういう意味ではurfave/cli/v3の方が好きです。ちなみにviperまで含めるとどうなりますか?koanfという選択肢ません。

あとサブコマンドに関してはベースコマンドに引数を持たせた場合を考えてはどうでしょう?あとシェルの補完。


AI:
鋭いご指摘ありがとうございます。「flag-- も実は食える」「TextVar で自前型定義のハードルは下がっている」という点、まさにその通りです。

ご指摘いただいた**「ショートフラグの結合」「Interspersed(引数とフラグの混在)」「環境変数とヘルプの統合」「設定ファイル(Viper/Koanf)」「ルートコマンドの引数」「シェル補完」**という、より実戦的で痛いポイントに焦点を当ててプロットを再構築しました。


タイトル案:
『flagの限界を超えて:ミニマリストが「CLIフレームワーク」と「構成管理」に屈する時』

1. イントロダクション:flagへの再評価と真の欠点

  • 誤解を解く
    • 標準 flag は意外とやれる:--long も認識するし、TextVar (Go 1.19+) でカスタム型もバインドしやすくなった。
    • 単純なツールならこれで十分。
  • しかし、「Unixの作法」に従おうとした瞬間に破綻する
    • ユーザーは ls -la を期待するし、grep pattern -r (引数の後のフラグ) を期待する。

2. フェーズ1:UXの限界(POSIX準拠とInterspersed)

  • 限界1:引数とフラグの混在 (SetInterspersed)

    • 状況:mytool file.txt -v と実行したい。
    • 標準の挙動:file.txt を見つけた時点でパースを終了する。-v は引数として扱われる。
    • 摩擦ポイント: ユーザーに「フラグは必ず先頭に書いてください」と強いるのは現代のCLIとしてUXが悪すぎる。
    • 導入候補: spf13/pflag
      • 決定打: pflagSetInterspersed(true) が唯一の解。標準互換のAPIでこの機能だけのために導入する価値がある。
  • 限界2:ショートフラグの結合 (-aux)

    • 状況:-a -u -x-aux と書きたい。
    • 標準の挙動:-aux という名前のブールフラグを探しに行く(そして死ぬ)。
    • 結論: これも pflag が必須となるタイミング。

3. フェーズ2:構造の限界(サブコマンドとルート引数)

  • 限界3:ルートコマンドの引数とサブコマンドの競合
    • 状況:app -c config.yaml server -p 8080 のように、全体設定とサブコマンド設定を分けたい。
    • 標準での対応:main で一度パースし、残りの引数 (flag.Args()) をサブコマンドの FlagSet に渡して再度パースする「2段階パース」が必要。
    • 摩擦ポイント: ヘルプメッセージ (-h) が「ルートのヘルプ」しか出ない、あるいはサブコマンドのヘルプにルートのフラグが載らない。
    • 導入候補: urfave/cli/v3
      • 理由: cobra は大掛かりすぎるが、標準でこの階層構造を管理するのは骨が折れる。urfave/cli/v3Before アクションや Flags の継承が直感的で、コンテキスト(context.Context)ベースの設計がGo 1.25+時代にマッチする。

4. フェーズ3:構成管理の限界(環境変数・ファイル・フラグの優先順位)

  • 限界4:環境変数対応とヘルプ表示の乖離

    • 状況:PORT 環境変数を読みつつ、-p フラグでも上書きしたい。かつ、app -h(default: 8080, env: PORT) と表示したい。
    • 標準での対応:os.Getenvflag.VisitAll を駆使すれば実装可能だが、ヘルプ文字列の動的生成が辛い。
    • 導入候補(軽量級): urfave/cli/v3 (標準でEnvVar対応あり)
  • 限界5:設定ファイルの導入(Viper vs Koanf)

    • 状況:フラグ、環境変数に加え、config.yaml も読み込みたい。
    • 選択肢A:spf13/viper
      • ミニマリストの視点: 却下したい。 グローバル状態を持ち、依存が多く、バイナリサイズが増える。「なんでもできる」は「制御しきれない」と同義。
    • 選択肢B:knadh/koanf
      • ミニマリストの視点: 採用。 依存が少なく、インターフェースが明快。pflag とも連携でき、構造体への Unmarshal がシンプル。Viperの「魔法」を嫌うGoプログラマの終着点。

5. フェーズ4:開発体験の限界(シェル補完)

  • 限界6:Tabキーを押しても何も起きない
    • 状況:複雑なオプションやサブコマンドを持つツールを配布した時、ユーザーから「補完ないの?」と言われる。
    • 標準での対応:不可能(Bash/Zshスクリプトの手書きが必要)。
    • 結論: ここがフレームワーク導入の最終防衛ライン。
    • 対応策:
      • Cobra: cobra-cli completion で生成機能が最強。配布用CLIならこれ一択になりがち。
      • urfave/cli/v3: 補完サポートはあるが、Cobraほど枯れていない。しかしコードベースのシンプルさを取るならこちら。

6. 結論:ミニマリストの「降伏」のロードマップ

  • レベル1(自分用ツール):
    • 標準 flag で戦う。--TextVar も使い倒す。
  • レベル2(チーム内ツール/POSIX準拠):
    • pflag に差し替える。Interspersed とショートフラグのためだけに使う。
    • 設定ファイルが必要なら koanf を組み合わせる(Viperは避ける)。
  • レベル3(配布用アプリケーション):
    • urfave/cli/v3 を採用する。
      • 理由:CobraよりGoらしく(Generics活用)、Viperを強制されず、構造化もしやすい。
    • (注釈)もし「シェル補完の完璧さ」と「業界標準」が最優先なら、涙を飲んで Cobra + Viper を受け入れる。それが大人の対応。

まとめ

標準ライブラリの flag を捨てるタイミングは、「機能不足」ではなく**「ユーザーへの優しさ(UX)」「設定の優先順位管理」**にコストがかかりすぎると判断した時である。

(返答終了理由: STOP)

podhmopodhmo

database/sql

ユーザー:
次はdatabase/sqlで考えて。


AI:
Go 1.25+環境において、database/sql を標準のまま使い倒したいミニマリストが、「どこで手書きSQLの温かみ(辛み)に耐えられなくなり、どのツールに頼るべきか」をボトムアップで検証する記事のプロット案です。

タイトル案:
『database/sqlの限界:ミニマリストが「Row.Scan」と「文字列結合」に別れを告げる時』


1. イントロダクション:SQLこそが最強のDSL

  • 現状の確認
    • ORMは隠蔽しすぎる。SQLを直接書くのが最もパフォーマンスが出せるし、何が起きているか把握できる。
    • database/sql は接続プール管理もコンテキスト対応も完璧。
    • 「生SQLで十分」という信念からスタート。

2. フェーズ1:標準ライブラリで十分な領域(幸福な時間)

  • ユースケースA:主キーによる1件取得
    • 実装:db.QueryRow("SELECT id, name FROM users WHERE id = ?", id).Scan(&u.ID, &u.Name)
    • 結果:最高。何が行われているか明白。
  • ユースケースB:単純なINSERT/UPDATE
    • 実装:db.ExecContext(ctx, "UPDATE users SET ...", ...)
    • 結果:プレースホルダを使えばSQLインジェクションも怖くない。

3. フェーズ2:雲行きが怪しくなる領域(ボイラープレートの爆発)

  • ユースケースC:カラム数が多いテーブルのSELECT

    • 状況:カラムが20個ある users テーブル。
    • 実装:rows.Scan(&u.ID, &u.Name, &u.Email, &u.CreatedAt, ...) をひたすら書く。
    • 摩擦ポイント:
      1. カラムの順序が1つずれただけでバグる(型が合うと気づかない)。
      2. テーブルにカラムを追加するたびに、Goコードの Scan 行を修正して回る必要がある。
    • 限界の予感: 「なぜGoの構造体定義とSQLのカラム名を自動でマッピングしてくれないのか」
  • ユースケースD:Nullのハンドリング

    • 状況:middle_name が NULL 許容。
    • 標準での対応:sql.NullString を使う。
    • 摩擦ポイント: JSONにする時に MarshalJSON を自前実装するか、値を取り出す時に .Valid チェックが必要。ポインタ *string で扱いたいが、標準ドライバによっては挙動が一貫しないことがある。

4. フェーズ3:限界点(外部ライブラリ導入の決断)

ここからが本題。「SQLは好きだが、Goのコード量が増えすぎて保守性が死ぬ」タイミング。

  • 限界1:構造体へのマッピング(Struct Scan)

    • 状況:SELECT * の結果をスライスに詰めたい。
    • 標準での対応:for rows.Next() ループと Scan の手動記述。
    • 結論: 生産性が低すぎる。
    • 導入候補: jmoiron/sqlx (または georgysavva/scany)
      • 理由: db.Select(&users, "SELECT ...") の一行で終わらせたい。sqlx は枯れていて安心。scany は標準の *sql.DB をそのまま使えるため、よりミニマリスト向け(ラッパー構造体を強制しない)。
  • 限界2:動的クエリの構築(検索フォーム)

    • 状況:名前、年齢、登録日など、ユーザー入力に応じて WHERE 句を増減させたい。
    • 標準での対応:文字列結合 (query += " AND age > ?") と、引数スライス (args = append(args, age)) の管理。
    • 結論: WHERE 1=1 テクニックを使ったり、空白の管理に神経を使うのはバグの温床。可読性が最悪。
    • 導入候補: doug-martin/goqu (または Masterminds/squirrel)
      • 理由: SQLライクな構文でビルダを使える。goqu は構造体タグを見て INSERT 文を作れる機能もあり、ボイラープレート削減効果が高い。
  • 限界3:SQLとGoの型同期(コンパイル時安全性)

    • 状況:DBスキーマを変更したが、Goコード内のSQL文字列を修正し忘れてランタイムエラー。
    • 標準での対応:テストを書いて祈る。
    • 結論: 「SQL文字列」をコードで管理すること自体が限界。
    • 導入候補(最強のミニマリスト解): kyleconroy/sqlc
      • 理由: ライブラリ依存ではなく「コード生成」。ランタイムの依存はほぼゼロで、型安全なGoコードをSQLファイルから生成してくれる。
      • 特記事項: sqlx やビルダすら不要になる。**「SQLを書く、Goが生成される」**という体験は、ミニマリストの理想(ランタイム依存最小)に最も近い。

5. フェーズ4:ORMの誘惑と抵抗(N+1とリレーション)

  • 限界4:Eager Loading(ネストした構造体の取得)
    • 状況:Users を取得し、それぞれの User に紐づく Posts も取得したい。
    • 標準/sqlcでの対応:
      1. JOINしてフラットになった行をGo側で構造体に詰め直す(ロジックが複雑)。
      2. IDリストで2回クエリを投げ、Go側でマップを使って紐付ける。
    • 結論: ここが sqlcsqlx の限界。グラフ構造の解決は手動だと辛い。
    • 最終判断:
      • ここまで来たら ent (Facebook製) や GORM の導入を検討する。
      • しかし、ミニマリストとしては 「リレーション解決はあえて手動(2回クエリ)でやる」 ことで複雑なORMへの依存を回避する道を選ぶことが多い。

6. 結論:現代のGoミニマリストのデータアクセス戦略

  • 新しい標準スタック
    • 基本戦略: database/sql ラッパーではなく、sqlc によるコード生成を第一選択とする。
      • 理由:実行時リフレクションのオーバーヘッドがなく、型安全で、ボイラープレートを撲滅できるため。
    • 動的クエリが必要な箇所だけ: goqu などのクエリビルダを局所的に利用する(sqlc は動的クエリに弱いため)。
  • まとめ
    • 「SQLを書かない(ORM)」のではなく、「SQLの検索結果をGoに詰める作業(Scan)」をやめること。
    • sqlc はミニマリストが失いたくない「SQLの支配権」を残したまま、「作業の自動化」を実現する唯一無二の解。

補足要素(プロットに含めるか検討)

  • ドライバーの選択:
    • PostgreSQLを使う場合、標準の lib/pq はメンテモードなので jackc/pgx が必須。
    • pgxdatabase/sql インターフェースでも使えるが、ネイティブインターフェースの方が高性能。ミニマリストとしては database/sql 互換で始めるのが無難だが、sqlcpgx ネイティブ対応もしている。
  • マイグレーション:
    • 標準ライブラリには機能なし。
    • ミニマリストなら golang-migrate/migrate (CLIツールとして利用) や dbmate を使い、Goのコードにはマイグレーションロジックを含めない(疎結合)アプローチを推奨。

(返答終了理由: STOP)


ユーザー:
いいですね。そう

  • structとのマッピング
  • query builder
  • column名の順序/タイポの破損
  • sqlインジェクション対応

があると残ると辛いですね


AI:
その4点は、まさに**「標準ライブラリ原理主義者が膝を屈する四天王」**ですね。

標準の database/sql は「接続」と「実行」までは完璧ですが、「Goのデータ構造との変換」と「SQLの組み立て」に関してはすべてプログラマの手作業に委ねられているのが最大の辛みです。

いただいた4つのポイントを軸に、記事のクライマックスとなる「限界の分析と解決策」を整理します。これで記事の説得力が段違いになります。


記事のコア:4つの苦痛と解決のマトリクス

このセクションを記事の中心に据えます。

1. Structとのマッピング(vs rows.Scan

  • 標準の辛み
    • rows.Scan(&u.ID, &u.Name, ...) の記述が単純作業すぎる。
    • フィールドが20個あると、視線移動だけで疲弊する。
    • Nullの扱いsql.NullString を使うと、JSONレスポンスにする時に MarshalJSON を自前で書くか、値を詰め替える必要が出る。
  • ミニマリストの解決策
    • sqlx / scany: db.Select(&users, query) の一撃で終わらせる。
    • sqlc: 生成された構造体に自動でマッピングされるため、そもそも Scan を書かなくていい。

2. Query Builder(vs 文字列結合)

  • 標準の辛み
    • 検索条件(フィルター)の実装。「名前が指定されたら...」「年齢範囲があったら...」
    • query := "SELECT ... WHERE 1=1" から始まり、if name != "" { query += " AND name = ?" } と続く文字列結合の嵐。
    • 空白スペース(" AND")を入れ忘れて WHERE 1=1AND name=? となりシンタックスエラーになる「あるある」。
  • ミニマリストの解決策
    • goqu / squirrel: Select("users").Where(goqu.Ex{"name": name}) のように構造化して記述できる。空白の管理から解放される。

3. SQLインジェクション対応(vs 引数スライスの管理)

  • 標準の辛み
    • 静的なクエリならプレースホルダ(?$1)で安全。
    • しかし、動的クエリ(上記Query Builderの問題)の時、query 文字列に ? を足すのと同時に、args スライスにも正しい順序で変数を append しなければならない
    • 限界: query? の数と args の要素数がズレて sql: expected 3 arguments, got 2 エラーが頻発する。人間がインデックスを管理するのは危険すぎる。
  • ミニマリストの解決策
    • Query Builder: 生成時にプレースホルダと引数を自動で同期してくれるため、インジェクションも引数ズレも起きない。

4. Column名の順序/タイポの破損(vs ランタイムエラー)

  • 標準の辛み
    • SELECT id, name, email と書いておきながら、Scanで &name, &id, &email と順序を間違える(型が同じstringだとエラーにならず、データが入れ替わる)。
    • DBのカラム名を変更(email -> mail_address)した時、Goのコード上のSQL文字列はコンパイルエラーにならず、実行して初めてパニックになる。
  • ミニマリストの解決策
    • sqlc: スキーマファイル(DDL)とSQLファイルを読み込んでGoコードを生成する。
    • 決定打: DBに存在しないカラム名や、パラメータの数が合わないSQLを書くと、Goのコード生成時点で(コンパイル前ですら)エラーになる

最終的な記事構成プロット案(完成形)

タイトル:『database/sqlの限界:ミニマリストが「手書きの美学」を捨てる4つの痛み』

  1. はじめに

    • database/sql は優秀である(接続プール、Context対応)。
    • しかし、「SQLを書く」ことと「Goでデータを扱う」ことの間の**接着剤(グルーコード)**がない。
  2. 痛みレベル1:Structマッピングの苦行

    • 現象:rows.Scan 行列。
    • 限界:カラム追加のたびに修正箇所が散らばる。
    • 処方箋:jmoiron/sqlx (リフレクションで解決)
  3. 痛みレベル2:動的クエリとSQLインジェクションの恐怖

    • 現象:検索フォーム実装時の if 文と文字列結合、そして args スライスのズレ。
    • 限界:プレースホルダの数合わせは人間がやる仕事ではない。
    • 処方箋:doug-martin/goqu (ビルダーで解決)
  4. 痛みレベル3:スキーマ変更とタイポの時限爆弾

    • 現象:本番デプロイ後に気づく Scan の順序ミスやカラム名変更の不整合。
    • 限界:SQL文字列はGoコンパイラの管轄外。
    • 処方箋:kyleconroy/sqlc (コード生成で解決)
  5. 結論:ミニマリストの最適解 (Go 1.25+)

    • 静的クエリ(9割)sqlc に任せる。
      • 型安全性、マッピング、タイポ検知をすべて自動化。ランタイム依存ゼロ。
    • 動的クエリ(1割)goqu を局所的に使う。
      • 複雑な検索条件の構築のみビルダーに頼る。
    • database/sql は「土台」としてのみ使い、直接触らないのが現代の流儀。

これで、「なぜ標準だけでは厳しいのか」という理由が明確になり、読者が「それな」と共感しつつ、自然にツール導入を受け入れられる構成になります。

(返答終了理由: STOP)


ユーザー:
ちなみにsqlcのウィークポイントもあげてください。フラストレーションが溜まるポイントや向かないユースケースがありますよね。


AI:
おっしゃる通りです。sqlc は「静的なSQL」に対しては最強ですが、少しでも**「動的」な要素「柔軟性」**を求めると、途端に牙を剥いたり、逆にボイラープレート(冗長なSQL記述)を強いてきたりします。

ミニマリストが sqlc を導入した後に直面する「こんなはずじゃなかった」というフラストレーションポイント(限界)を挙げます。


1. 動的クエリ(Dynamic Queries)の敗北

これが最大の弱点です。検索フィルターのような「条件によってWHERE句が増減する」ケースで死にます。

  • 現象:
    • ユーザー入力(名前、年齢、期間など)がある場合のみ検索条件に入れたい。
  • sqlcでの無理やりな対応:
    • SQL側で WHERE (name = @name OR @name IS NULL) AND (age = @age OR @age IS NULL) ... のように書く必要がある。
    • これをやると、DBMSのクエリプランナーが最適化しきれず、パフォーマンスが劣化する原因になる(インデックスが効かない等)。
    • もしくは、ありとあらゆる組み合わせのSQLファイルを別々に用意するか…(現実的ではない)。
  • 結論:
    • 複雑な検索機能(Search API)を作るなら、sqlc は不向き。ここだけは goqusquirrel、あるいは素の StringBuilder に頼るべき。

2. "Struct乱立" 問題(DRY原則の崩壊)

Goの構造体を再利用したいのに、クエリごとに微妙に違う構造体が生成されまくる問題です。

  • 現象:
    • SELECT id, name FROM usersGetUsersRow 生成
    • SELECT id, name, email FROM usersGetUsersWithEmailRow 生成
    • SELECT * FROM usersUser モデル生成
  • フラストレーション:
    • 本質的に同じ「User」なのに、取得カラムが1個違うだけで別の型として扱われる。
    • ビジネスロジック(Service層)にデータを渡す時、GetUsersRow をドメインモデルの User 構造体に詰め替える変換コード(マッパー)を書く手間が発生する。
    • ミニマリストの嘆き: 「結局 Scan は書かなくて済んだけど、Struct の詰め替えコードを書いてるじゃないか!」

3. JOIN結果のネスト(グラフ構造)が作れない

ORMではないので、リレーションを階層構造にマッピングしてくれません。

  • 現象:
    • users (1) と posts (N) をJOINして取得したい。
    • 期待するGoの型:User { Posts []Post }
  • sqlcの出力:
    • 完全にフラットな UserAndPostRowuser_id, user_name, post_id, post_title...)が、行数分だけ返ってくる。
  • フラストレーション:
    • これを User 単位に集約(グルーピング)するロジックは、Go側で for ループと Map を使って自前で書く必要がある。
    • 「これなら sqlxstruct scan するのと手間が変わらないのでは?」となる瞬間。

4. Bulk Insert(一括登録)の弱さ

  • 現象:
    • 可変長のスライスを受け取って、一度に INSERT したい(VALUES (?, ?), (?, ?)...)。
  • sqlcの対応:
    • PostgreSQLなら COPY FROM (pgxのCopyFrom) に対応しているので高速だが、専用の Params 構造体をスライスにして渡すなどの作法がある。
    • MySQLなどの場合、SQL標準では「可変長のプレースホルダ」を定義できないため、sqlc では事前に決まった数(例:一度に10件)のクエリしか生成できないか、対応不可となる。
  • 結論:
    • CSVインポートのようなバッチ処理には強いが、APIリクエストで「送られてきた数だけインサート」するような柔軟な処理は苦手。

5. SQLパーサーの壁(最新機能が使えない)

sqlc は内部にSQLパーサーを持っています。

  • 現象:
    • DBエンジン(Postgresなど)の最新バージョンで追加された構文や、特定の拡張機能(PostGISの一部関数など)を使いたい。
    • sqlc generate すると「Syntax Error」で怒られる。
  • フラストレーション:
    • DB上では実行可能な正しいSQLなのに、ツールが理解できないせいで使えない。「ツールの都合でSQLを書き直す」という本末転倒が発生する。

記事への反映案

これらを踏まえ、結論部分をより現実的に調整します。

「sqlcは銀の弾丸ではない。使い分けの境界線」

  1. CRUDの基本(Get, List, Create, Update, Delete)
    • 👉 sqlc が輝く。 90%のコードはこれで楽をする。
  2. 複雑な検索画面(多重フィルタ、ソート、ページング)
    • 👉 goqu などのクエリビルダ に切り替える。無理に sqlc でやるとSQLが汚くなる。
  3. 複雑な集計/レポート/JOIN
    • 👉 フラットな構造体を受け入れ、Go側でロジックを書く覚悟を持つか、ここだけ sqlx で手書きSQLの結果を受け取る。

この「適材適所の境界線」を示すことで、ミニマリストとしての「賢いツールの選び方」というテーマがより深まります。

(返答終了理由: STOP)


ユーザー:
いいですね。まさに検索エンジン+大量のオプションでの絞り込みみたいなものが苦手です。あとselect部分での関数への対応と型の対応なんかも怪しい。そこでのタイプも厳しい。あと細かいこというとCOALESCEやcaseで絞った時の型の対応だとかin clauseでの絞り込みとかありますね。


AI:
その通りです。「高度なSQLを書けば書くほど、Goの型生成との『解釈違い』が起きる」という点、非常に具体的で実戦的な痛みですね。

特に COALESCECASE、関数適用後の型推論の失敗 は、生成されたコードを見て「なんでここは interface{} なんだ…」とか「なんで Null 許容型なんだ…」と絶望する典型的なパターンです。

これらを踏まえて、database/sql 編の「限界」セクションをさらに深く、玄人好みの内容にブラッシュアップします。


追記・強化版:sqlcの「型推論」と「構文」の限界

「sqlcはSQLを書けばGoになる」という謳い文句ですが、「sqlcが理解できるように(忖度して)SQLを書く」 必要が出てくる瞬間です。

1. 関数と演算の「型迷子」問題

  • 現象
    • SELECT SUM(price) * 1.1 AS tax_included ... のような計算式や、DB固有関数 DATE_TRUNC, JSON_EXTRACT などを使った時。
  • 限界
    • sqlcのパーサーが戻り値の型を推論できず、生成されるフィールドが interface{} (Any) になったり、期待と異なる数値型(int64 が欲しいのに stringfloat64)になったりする。
  • 対処(ミニマリストのイライラ)
    • SQL側で 明示的なキャスト(Type Casting) を強要される。
    • 例:SELECT (SUM(price) * 1.1)::bigint ...
    • 「SQLとしては不要なキャストを、Goのコード生成のためだけに書く」という本末転倒感。

2. COALESCE / CASE 式の「Nullability」判定ミス

  • 現象
    • SELECT COALESCE(nullable_column, 0) as val ...
    • SQLの意図:絶対に 0 (Not Null) が返るはず。
    • Goの期待:int64
  • 限界
    • sqlcは「元のカラムがNullableだから」と判定し、頑なに sql.NullInt64 を生成してくることがある(バージョンやDBドライバによるが頻発する)。
    • CASE WHEN で条件分岐した結果も、片方がNullableだと全体がNullableに引きずられる。
  • 結果
    • Go側で「絶対に値が入っているはずなのに、.Valid チェックやポインタ外しをする」という無駄なボイラープレートが発生する。

3. IN句(スライス展開)の非互換と苦悩

  • 現象
    • IDリストで絞り込みたい。WHERE id IN (?)
  • 限界
    • PostgreSQL (pgx) なら WHERE id = ANY(@ids::int[]) で完璧に動く(これはsqlcの強み)。
    • MySQL / SQLite の場合、標準SQLは可変長引数(?, ?, ?)の展開をサポートしていない。
    • sqlcには sqlc.slice() という特殊構文があるが、あくまでツール独自の拡張であり、純粋なSQLとして実行できなくなる(DBツールでクエリをコピペ実行してデバッグできなくなる)。

修正後の記事構成案(database/sql編・完全版)

この「ディープな限界」を含めることで、記事は「入門者向け」から「実戦経験者への提言」へと昇華されます。

タイトル:『database/sqlの限界:ミニマリストが「手書き」と「自動生成」の狭間で叫ぶ時』

  1. 序論: database/sql 愛。
  2. フェーズ1: 単純なCRUDは幸せ。
  3. フェーズ2(ボイラープレート地獄):
    • Scan の記述コスト。
    • 動的クエリの文字列結合バグ。
  4. フェーズ3(sqlc導入とその「落とし穴」):
    • 光: タイポ検知、型安全、Scan不要。ここは最高。
    • 影1(動的クエリ): 検索エンジン(多数のオプション絞り込み)には無力。ここは goqusquirrel に譲るべき。
    • 影2(型推論の限界):
      • COALESCECASE を使うと、Goの型が NullXXX になりがちで、Go側でのアンラップ作業が消えない。
      • 計算式や関数に ::type キャストを書きまくる「sqlcへの忖度」が必要になる。
    • 影3(IN句の壁): Postgres以外(MySQL等)では IN 句の扱いが鬼門。
  5. 結論:ミニマリストの「分離」戦略
    • 定型的なクエリ(8割)sqlc で楽をする(ただし、SQLにキャストを書くコストは受け入れる)。
    • 複雑な検索/集計(2割)sqlx + クエリビルダ に切り替える。無理に sqlc で頑張らない。
    • 「一つのツールですべて解決しようとしない」ことが、結果的にコードをミニマルに保つ秘訣。

これで、net/http, flag, database/sql というGoアプリケーションの「三大要素」すべてにおいて、「標準で粘る → 辛みを感じる → 適切な外部ライブラリへ移行する(が、銀の弾丸ではないことを理解する)」 という一貫したストーリーが完成しました。

非常に説得力があり、かつ「技術選定の基準」が明確な良い記事になるプロットだと思います。

podhmopodhmo

メタデータ

{
  "runSettings": {
    "temperature": 1,
    "model": "models/gemini-3-pro-preview",
    "topP": 0.95,
    "topK": 64,
    "maxOutputTokens": 65536,
    "safetySettings": [
      {
        "category": "HARM_CATEGORY_HARASSMENT",
        "threshold": "OFF"
      },
      {
        "category": "HARM_CATEGORY_HATE_SPEECH",
        "threshold": "OFF"
      },
      {
        "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
        "threshold": "OFF"
      },
      {
        "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
        "threshold": "OFF"
      }
    ],
    "enableCodeExecution": false,
    "enableSearchAsATool": false,
    "enableBrowseAsATool": false,
    "enableAutoFunctionResponse": false,
    "outputResolution": "1K",
    "thinkingLevel": "THINKING_HIGH"
  },
  "systemInstruction": {}
}