メインコンテンツへスキップ

Go Proposal Weekly Digest

Go言語のproposal更新を毎週お届け

#62146declined

database/sql: export convertAssign as DefaultConvertAssign

ステータス変更: active declined

要約

AIによる要約であり、誤りを含む場合があります。

概要

database/sql パッケージ内部の非公開関数 convertAssignDefaultConvertAssign として公開する提案です。この提案は重複する上位issue (#67546) が「likely accept」に移行したため、重複として却下(declined as duplicate)されました。

ステータス変更

activedeclined
convertAssign のエクスポートはissue #67546 の議論の中で包括的に扱われており、そちらで sql.ConvertAssign という形で公開APIとして採用される方向で決着しました。@aclements が本issueを重複として閉じ、#67546 で引き続き議論・実装が進む旨をアナウンスしました。

技術的背景

現状の問題点

database/sqlRows.Scan は内部で convertAssign という関数を使って、データベースドライバから返された値をGoの型に変換します。この関数は強力な型変換ロジックを持ちますが、非公開(unexported)であるため外部から利用できません。
カスタム型(Optional型、ジェネリック型ラッパーなど)で sql.Scanner インターフェースを実装しようとする開発者は、convertAssign に相当するロジックを自前で再実装するか、非推奨の go:linkname ハックで直接参照するしかありませんでした。
かつては関数をコピーして使うことも可能でしたが、現在の実装は非公開な型や関数(例: driverArgsConnLockedRawBytes の特殊処理など)に依存しており、単純なコピーが不可能な状態です。

// 問題: sql.Scannerを実装するカスタムOptional型
type Optional[T any] struct { ... }
func (o *Optional[T]) Scan(src any) error {
    // ここで convertAssign 相当のロジックが必要だが、
    // 外部からアクセスできないため再実装が困難
    // => go:linkname ハックや不完全なコピーに頼らざるを得ない
}

また、提案者(@majewsky)が指摘したように、driver.Valuer 方向(GoからSQL)は driver.DefaultParameterConverter という形で公開APIが存在するのに対し、Scan 方向(SQLからGo)には相当する公開APIが存在しないという非対称性も問題でした。

提案された解決策

convertAssignDefaultConvertAssign(または ConvertAssign)という名前で公開する、というシンプルな提案でした。
本issueは2023年8月に起票され、2025年12月にactiveレビュー対象となりました。その後、より広範なissue #67546(ドライバが Scan 処理全体をオーバーライドできるようにする提案)との統合議論が行われた結果、#67546 の実装CL(go.dev/cl/766701)の中で以下のAPIとして収束しました。

package sql
// ConvertAssign copies the value in src to the value pointed at by dest.
// See the documentation on [Rows.Scan] for details on conversions.
// dest must be a pointer or must implement [Scanner].
//
// ConvertAssign is intended for use by driver implementations.
// Most users should not need to use it directly.
func ConvertAssign(dest any, src driver.Value) error

あわせて database/sql/driver パッケージに以下のインターフェースが追加される予定です。

package driver
type RowsColumnScanner interface {
    Rows
    NextRow() error
    ScanColumn(index int, dest any) error
}

これによって何ができるようになるか

sql.ConvertAssign が公開されることで、カスタム型を実装する開発者がGoの標準的な型変換ロジックを再利用できるようになります。

コード例

// Before: go:linkname ハックや不完全なコピーが必要だった
//go:linkname convertAssign database/sql.convertAssign
func convertAssign(dest, src any) error // ハック、非推奨
// After: 公式APIとして利用可能
func (o *Optional[T]) Scan(src any) error {
    val := new(T)
    // 公式のConvertAssignで標準的な型変換を委譲
    if err := sql.ConvertAssign(val, src.(driver.Value)); err != nil {
        return err
    }
    o.Set(*val)
    return nil
}

具体的なユースケースとして以下が挙げられます。

  1. Optional/Nullable型の実装: Goのジェネリクスを活用した Optional[T] 型でDatabaseのNULL値を扱う際、Scan メソッド内で標準の変換ロジックを再利用できる
  2. 構造体スキャナーライブラリ: Rows.Scan の型変換ロジックを活用した独自スキャンライブラリの実装(SQLデータをGoの構造体に自動マッピングするORM的な処理)
  3. カスタムドライバの実装: RowsColumnScanner を実装するドライバが、対応できない型変換をGoの標準ロジックにフォールバックする際に ConvertAssign を呼び出す

議論のハイライト

  • 2023年の先行提案での却下経緯: issue #24258(2018年)および #35697(2019年)で同様の提案が @kardianos@rsc によって却下されており、「関数をコピーして使うように」という判断でした。しかしその後の実装変更で非公開型への依存が増し、単純コピーが不可能になったことが本提案の再提起の根拠となりました
  • driver.DefaultParameterConverter との非対称性: SQLへの書き込み方向は公開APIが存在するのに読み取り方向(Scan)は存在しないという指摘は、提案の正当性を支持する重要な論点でした
  • #67546との統合: @aclements が本issueを #67546 と合わせて検討することを表明し、最終的には RowsColumnScanner インターフェース(ドライバが Scan を完全に制御できるメカニズム)と ConvertAssign の公開をセットで解決する方針に収束しました
  • RawBytesの特殊処理: 公開APIの設計上、*sql.RawBytes 宛の代入で Rows がバッファを行間で再利用する最適化が一部失われる点が議論されました(スタンドアロンで ConvertAssign を呼ぶ場合はコンテキストがないため)
  • 「重複として却下」という判断: 本issueが実質的に解決済みとなったため、新たな議論を開かずに #67546 へ誘導する形で閉じられました。Proposalが「declined」でも機能自体は別issueで実現される点に注意が必要です

関連リンク