Goの'sql: Scan error on column index X, name: converting NULL to X is an invalid operation'を修正する

beginner🔷 Go2026-04-02| Go 1.16以降、database/sqlパッケージ、MySQL / PostgreSQL / SQLite、Linux / macOS / Windows

Error Message

sql: Scan error on column index X, name "column_name": converting NULL to string is an invalid operation
#go#golang#データベース#sql#null#scan#mysql#postgresql

エラーの内容

クエリを実行し、rows.Scan()を呼び出すと、Goから以下のエラーが返されます:

sql: Scan error on column index 2, name "description": converting NULL to string is an invalid operation

または、類似した以下のエラーも発生します:

sql: Scan error on column index 1, name "age": converting NULL to int64 is an invalid operation
sql: Scan error on column index 3, name "active": converting NULL to bool is an invalid operation

クエリ自体は正常に実行されています。構文エラーもネットワークの問題もありません。クラッシュは純粋にスキャン時に発生し、返された列のいずれかにSQL NULLが含まれている場合に起こります。

根本原因

Goのプリミティブ型 — stringintboolfloat64 — には「値なし」という概念がありません。これらは常に何らかの値を保持します。database/sqlがデータベースからNULLをこれらの型にコピーしようとすると、格納する場所がないためエラーを返します。

このエラーが確実に発生するケースはいくつかあります:

  • スキーマ上でNULL許容列(VARCHAR NULLINT NULL)であるにもかかわらず、素のstringintにスキャンしている場合。
  • LEFT JOINにより、右側テーブルの一致しない行に対してNULLが返される場合。
  • デフォルト値のない列に、その列を指定せずに行が挿入された場合。
  • 本番環境でALTER TABLE ... MODIFY COLUMN ... NULLが実行されたが、Goのコードが更新されていない場合。

再現方法

type User struct {
    ID          int
    Name        string
    Description string // DBではNULL許容
}

row := db.QueryRow("SELECT id, name, description FROM users WHERE id = ?", 1)
var u User
err := row.Scan(&u.ID, &u.Name, &u.Description) // descriptionがNULLの場合クラッシュ
if err != nil {
    log.Fatal(err) // sql: Scan error on column index 2, name "description": converting NULL to string is an invalid operation
}

解決策1:sql.Null*型を使用する(推奨)

database/sqlパッケージには、まさにこのような状況のために設計されたNULL許容ラッパーが付属しています。NULL許容列に対応するプリミティブ型を以下と入れ替えます:

  • sql.NullString
  • sql.NullInt64 / sql.NullInt32 / sql.NullInt16
  • sql.NullFloat64
  • sql.NullBool
  • sql.NullTime
import "database/sql"

type User struct {
    ID          int
    Name        string
    Description sql.NullString // NULLを保持できる
    Age         sql.NullInt64
}

row := db.QueryRow("SELECT id, name, description, age FROM users WHERE id = ?", 1)
var u User
err := row.Scan(&u.ID, &u.Name, &u.Description, &u.Age)
if err != nil {
    log.Fatal(err)
}

// 値を読み取る前に必ず.Validを確認する
if u.Description.Valid {
    fmt.Println("Description:", u.Description.String)
} else {
    fmt.Println("DescriptionはNULLです")
}

sql.Null*型は2つのフィールドを持ちます:.Valid(非NULLかどうか)と実際の値(.String.Int64など)。追加の依存関係やサードパーティライブラリは不要です。

解決策2:ポインタにスキャンする

構造体を変更したくない場合は、代わりにポインタにスキャンします。nilポインタはNULLに対して完全に有効なスキャンターゲットです:

var description *string
var age *int64

err := row.Scan(&id, &name, &description, &age)
if err != nil {
    log.Fatal(err)
}

if description != nil {
    fmt.Println(*description)
}

他の部分を変更せずに、1つの関数・1つのクエリだけをピンポイントで修正したい場合に有効です。テーブル全体への変更には、解決策1の方がスケールします。

解決策3:SQLでCOALESCEを使用してNULLを根本から回避する

最もクリーンな修正が上流にある場合もあります。アプリケーション内でNULLと空文字列(またはゼロ)が同じ意味を持つ場合、データがGoに渡る前にデフォルト値に置き換えるようクエリを書き直します:

SELECT id, name, COALESCE(description, '') AS description, COALESCE(age, 0) AS age
FROM users
WHERE id = ?;

var description string // 安全 — COALESCEが非NULL値を保証する
var age int64

err := row.Scan(&id, &name, &description, &age)

NULLとゼロの区別が実際に重要な場合(例:スコア0と未入力のスコアの違い)はこの方法を避けてください。

複数のNULL許容列への対処

6〜8列ものNULL許容列があるテーブルはすぐに混乱します。一つのクリーンなパターン:SQL結果をそのまま反映する専用のスキャン構造体を定義し、その後でドメイン構造体にマッピングします。

type userRow struct {
    ID          int
    Name        string
    Description sql.NullString
    Age         sql.NullInt64
    Score       sql.NullFloat64
    Active      sql.NullBool
    CreatedAt   sql.NullTime
}

func scanUser(row *sql.Row) (*User, error) {
    var r userRow
    err := row.Scan(&r.ID, &r.Name, &r.Description, &r.Age, &r.Score, &r.Active, &r.CreatedAt)
    if err != nil {
        return nil, err
    }
    u := &User{
        ID:   r.ID,
        Name: r.Name,
    }
    if r.Description.Valid {
        u.Description = r.Description.String
    }
    if r.Age.Valid {
        u.Age = int(r.Age.Int64)
    }
    return u, nil
}

スキャン構造体がNULL処理のノイズをすべて吸収します。Userドメイン構造体はクリーンなままを保てます。

NULL許容列の特定方法

どの列がNULLを返す可能性があるか分からない場合は、データベースに直接問い合わせます:

-- MySQL / MariaDB
SELECT COLUMN_NAME, IS_NULLABLE, DATA_TYPE
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = 'your_db' AND TABLE_NAME = 'users';

-- PostgreSQL
SELECT column_name, is_nullable, data_type
FROM information_schema.columns
WHERE table_name = 'users';

IS_NULLABLE = YESと表示される行は、Go側でNULL許容のスキャンターゲットが必要です。見慣れないテーブルのスキャンコードを書く前に、この出力を確認してください。

動作確認

修正を適用したら、データのある行だけでなく、実際のNULL行に対してもテストします:

-- NULLを含むテスト行を挿入
INSERT INTO users (id, name, description) VALUES (999, 'Test', NULL);

row := db.QueryRow("SELECT id, name, description FROM users WHERE id = ?", 999)
var id int
var name string
var description sql.NullString

err := row.Scan(&id, &name, &description)
if err != nil {
    log.Fatalf("まだ壊れています: %v", err)
}
fmt.Printf("ID: %d, Name: %s, Description valid: %v\n", id, name, description.Valid)
// 出力: ID: 999, Name: Test, Description valid: false

エラーなし、かつValid: false — これが修正が実際に機能していることの確認です。

得られた教訓

  • **スキャンターゲットを書く前にスキーマのNULL許容性を確認すること。**列がNULLを許容する場合、GoのコードもNULLを許容する型を使用する必要があります。
  • **LEFT JOINはほぼ必ずNULLを生成します。**右側テーブルのすべての列がNULL許容型の候補となります。
  • **目的に合ったツールを選ぶこと:**NULLとゼロが異なる意味を持つ場合はsql.Null*型を使用し、素早いローカル修正にはポインタを使用し、意味が同じ場合はCOALESCEを使用します。
  • **テストデータにNULLを含めること。**完全なデータの行しか挿入しないテストスイートでは、このクラスのバグは本番環境で発見されるまで決して検出されません。

Related Error Notes